Skip to content

Commit

Permalink
feat: delete multiple resources in parallel
Browse files Browse the repository at this point in the history
  • Loading branch information
phm07 committed May 23, 2024
1 parent 01ba7a9 commit 01eac0c
Show file tree
Hide file tree
Showing 15 changed files with 135 additions and 78 deletions.
63 changes: 43 additions & 20 deletions internal/cmd/base/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package base
import (
"errors"
"fmt"
"reflect"
"strings"
"sync"

"github.com/spf13/cobra"

Expand All @@ -17,11 +18,12 @@ import (
// DeleteCmd allows defining commands for deleting a resource.
type DeleteCmd struct {
ResourceNameSingular string // e.g. "server"
ResourceNamePlural string // e.g. "servers"
ShortDescription string
NameSuggestions func(client hcapi2.Client) func() []string
AdditionalFlags func(*cobra.Command)
Fetch func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error)
Delete func(s state.State, cmd *cobra.Command, resource interface{}) error
Delete func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error)
}

// CobraCommand creates a command that can be registered with cobra.
Expand Down Expand Up @@ -49,29 +51,50 @@ func (dc *DeleteCmd) CobraCommand(s state.State) *cobra.Command {
return cmd
}

// Run executes a describe command.
// Run executes a delete command.
func (dc *DeleteCmd) Run(s state.State, cmd *cobra.Command, args []string) error {
var cmdErr error

for _, idOrName := range args {
resource, _, err := dc.Fetch(s, cmd, idOrName)
if err != nil {
cmdErr = errors.Join(cmdErr, err)
continue
}
wg := sync.WaitGroup{}
wg.Add(len(args))
actions, errs :=
make([]*hcloud.Action, len(args)),
make([]error, len(args))

// resource is an interface that always has a type, so the interface is never nil
// (i.e. == nil) is always false.
if reflect.ValueOf(resource).IsNil() {
cmdErr = errors.Join(cmdErr, fmt.Errorf("%s not found: %s", dc.ResourceNameSingular, idOrName))
continue
}
for i, idOrName := range args {
i, idOrName := i, idOrName
go func() {
defer wg.Done()
resource, _, err := dc.Fetch(s, cmd, idOrName)
if err != nil {
errs[i] = err
return
}
if util.IsNil(resource) {
errs[i] = fmt.Errorf("%s not found: %s", dc.ResourceNameSingular, idOrName)
return
}
actions[i], errs[i] = dc.Delete(s, cmd, resource)
}()
}

if err = dc.Delete(s, cmd, resource); err != nil {
cmdErr = errors.Join(cmdErr, fmt.Errorf("deleting %s %s failed: %s", dc.ResourceNameSingular, idOrName, err))
wg.Wait()
filtered := util.FilterNil(actions)
var err error
if len(filtered) > 0 {
err = s.WaitForActions(cmd, s, util.FilterNil(actions)...)
}

var actuallyDeleted []string
for i, idOrName := range args {
if errs[i] == nil {
actuallyDeleted = append(actuallyDeleted, idOrName)
}
cmd.Printf("%s %v deleted\n", dc.ResourceNameSingular, idOrName)
}

return cmdErr
if len(actuallyDeleted) == 1 {
cmd.Printf("%s %s deleted\n", dc.ResourceNameSingular, actuallyDeleted[0])
} else if len(actuallyDeleted) > 1 {
cmd.Printf("%s %s deleted\n", dc.ResourceNamePlural, strings.Join(actuallyDeleted, ", "))
}
return errors.Join(append(errs, err)...)
}
1 change: 1 addition & 0 deletions internal/cmd/base/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

var fakeDeleteCmd = &base.DeleteCmd{
ResourceNameSingular: "Fake resource",
ResourceNamePlural: "Fake resources",
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {

Check failure on line 18 in internal/cmd/base/delete_test.go

View workflow job for this annotation

GitHub Actions / lint

cannot use func(s state.State, cmd *cobra.Command, resource interface{}) error {…} (value of type func(s state.State, cmd *cobra.Command, resource interface{}) error) as func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) value in struct literal (typecheck)

Check failure on line 18 in internal/cmd/base/delete_test.go

View workflow job for this annotation

GitHub Actions / test

cannot use func(s state.State, cmd *cobra.Command, resource interface{}) error {…} (value of type func(s state.State, cmd *cobra.Command, resource interface{}) error) as func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) value in struct literal
cmd.Println("Deleting fake resource")
return nil
Expand Down
9 changes: 4 additions & 5 deletions internal/cmd/certificate/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@ import (

var DeleteCmd = base.DeleteCmd{
ResourceNameSingular: "certificate",
ResourceNamePlural: "certificates",
ShortDescription: "Delete a certificate",
NameSuggestions: func(c hcapi2.Client) func() []string { return c.Firewall().Names },
Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) {
return s.Client().Certificate().Get(s, idOrName)
},
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) {
certificate := resource.(*hcloud.Certificate)
if _, err := s.Client().Certificate().Delete(s, certificate); err != nil {
return err
}
return nil
_, err := s.Client().Certificate().Delete(s, certificate)
return nil, err
},
}
9 changes: 4 additions & 5 deletions internal/cmd/firewall/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@ import (

var DeleteCmd = base.DeleteCmd{
ResourceNameSingular: "firewall",
ResourceNamePlural: "firewalls",
ShortDescription: "Delete a firewall",
NameSuggestions: func(c hcapi2.Client) func() []string { return c.Firewall().Names },
Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) {
return s.Client().Firewall().Get(s, idOrName)
},
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) {
firewall := resource.(*hcloud.Firewall)
if _, err := s.Client().Firewall().Delete(s, firewall); err != nil {
return err
}
return nil
_, err := s.Client().Firewall().Delete(s, firewall)
return nil, err
},
}
9 changes: 4 additions & 5 deletions internal/cmd/floatingip/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@ import (

var DeleteCmd = base.DeleteCmd{
ResourceNameSingular: "Floating IP",
ResourceNamePlural: "Floating IPs",
ShortDescription: "Delete a Floating IP",
NameSuggestions: func(c hcapi2.Client) func() []string { return c.FloatingIP().Names },
Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) {
return s.Client().FloatingIP().Get(s, idOrName)
},
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) {
floatingIP := resource.(*hcloud.FloatingIP)
if _, err := s.Client().FloatingIP().Delete(s, floatingIP); err != nil {
return err
}
return nil
_, err := s.Client().FloatingIP().Delete(s, floatingIP)
return nil, err
},
}
9 changes: 4 additions & 5 deletions internal/cmd/image/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@ import (

var DeleteCmd = base.DeleteCmd{
ResourceNameSingular: "image",
ResourceNamePlural: "images",
ShortDescription: "Delete an image",
NameSuggestions: func(c hcapi2.Client) func() []string { return c.Image().Names },
Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) {
return s.Client().Image().Get(s, idOrName)
},
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) {
image := resource.(*hcloud.Image)
if _, err := s.Client().Image().Delete(s, image); err != nil {
return err
}
return nil
_, err := s.Client().Image().Delete(s, image)
return nil, err
},
}
9 changes: 4 additions & 5 deletions internal/cmd/loadbalancer/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@ import (

var DeleteCmd = base.DeleteCmd{
ResourceNameSingular: "Load Balancer",
ResourceNamePlural: "Load Balancers",
ShortDescription: "Delete a Load Balancer",
NameSuggestions: func(c hcapi2.Client) func() []string { return c.LoadBalancer().Names },
Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) {
return s.Client().LoadBalancer().Get(s, idOrName)
},
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) {
loadBalancer := resource.(*hcloud.LoadBalancer)
if _, err := s.Client().LoadBalancer().Delete(s, loadBalancer); err != nil {
return err
}
return nil
_, err := s.Client().LoadBalancer().Delete(s, loadBalancer)
return nil, err
},
}
9 changes: 4 additions & 5 deletions internal/cmd/network/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@ import (

var DeleteCmd = base.DeleteCmd{
ResourceNameSingular: "Network",
ResourceNamePlural: "Networks",
ShortDescription: "Delete a network",
NameSuggestions: func(c hcapi2.Client) func() []string { return c.Network().Names },
Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) {
return s.Client().Network().Get(s, idOrName)
},
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) {
network := resource.(*hcloud.Network)
if _, err := s.Client().Network().Delete(s, network); err != nil {
return err
}
return nil
_, err := s.Client().Network().Delete(s, network)
return nil, err
},
}
9 changes: 4 additions & 5 deletions internal/cmd/placementgroup/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@ import (

var DeleteCmd = base.DeleteCmd{
ResourceNameSingular: "placement group",
ResourceNamePlural: "placement groups",
ShortDescription: "Delete a placement group",
NameSuggestions: func(c hcapi2.Client) func() []string { return c.PlacementGroup().Names },
Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) {
return s.Client().PlacementGroup().Get(s, idOrName)
},
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) {
placementGroup := resource.(*hcloud.PlacementGroup)
if _, err := s.Client().PlacementGroup().Delete(s, placementGroup); err != nil {
return err
}
return nil
_, err := s.Client().PlacementGroup().Delete(s, placementGroup)
return nil, err
},
}
9 changes: 4 additions & 5 deletions internal/cmd/primaryip/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@ import (

var DeleteCmd = base.DeleteCmd{
ResourceNameSingular: "Primary IP",
ResourceNamePlural: "Primary IPs",
ShortDescription: "Delete a Primary IP",
NameSuggestions: func(c hcapi2.Client) func() []string { return c.PrimaryIP().Names },
Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) {
return s.Client().PrimaryIP().Get(s, idOrName)
},
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) {
primaryIP := resource.(*hcloud.PrimaryIP)
if _, err := s.Client().PrimaryIP().Delete(s, primaryIP); err != nil {
return err
}
return nil
_, err := s.Client().PrimaryIP().Delete(s, primaryIP)
return nil, err
},
}
12 changes: 4 additions & 8 deletions internal/cmd/server/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,18 @@ import (

var DeleteCmd = base.DeleteCmd{
ResourceNameSingular: "Server",
ResourceNamePlural: "Servers",
ShortDescription: "Delete a server",
NameSuggestions: func(c hcapi2.Client) func() []string { return c.Server().Names },
Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) {
return s.Client().Server().Get(s, idOrName)
},
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) {
server := resource.(*hcloud.Server)
result, _, err := s.Client().Server().DeleteWithResult(s, server)
if err != nil {
return err
return nil, err
}

if err := s.WaitForActions(cmd, s, result.Action); err != nil {
return err
}

return nil
return result.Action, nil
},
}
9 changes: 4 additions & 5 deletions internal/cmd/sshkey/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@ import (

var DeleteCmd = base.DeleteCmd{
ResourceNameSingular: "SSH Key",
ResourceNamePlural: "SSH Keys",
ShortDescription: "Delete a SSH Key",
NameSuggestions: func(c hcapi2.Client) func() []string { return c.SSHKey().Names },
Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) {
return s.Client().SSHKey().Get(s, idOrName)
},
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) {
sshKey := resource.(*hcloud.SSHKey)
if _, err := s.Client().SSHKey().Delete(s, sshKey); err != nil {
return err
}
return nil
_, err := s.Client().SSHKey().Delete(s, sshKey)
return nil, err
},
}
24 changes: 24 additions & 0 deletions internal/cmd/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"os"
"reflect"
"sort"
"strings"
"text/template"
Expand Down Expand Up @@ -209,3 +210,26 @@ func AddGroup(cmd *cobra.Command, id string, title string, groupCmds ...*cobra.C
func ToKebabCase(s string) string {
return strings.ReplaceAll(strings.ToLower(s), " ", "-")
}

func IsNil(v any) bool {
if v == nil {
return true
}
val := reflect.ValueOf(v)
switch val.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return val.IsNil()
default:
return false
}
}

func FilterNil[T any](values []T) []T {
var filtered []T
for _, v := range values {
if !IsNil(v) {
filtered = append(filtered, v)
}
}
return filtered
}
23 changes: 23 additions & 0 deletions internal/cmd/util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,26 @@ func TestToKebabCase(t *testing.T) {
assert.Equal(t, "foo-bar", util.ToKebabCase("Foo Bar"))
assert.Equal(t, "foo", util.ToKebabCase("Foo"))
}

func TestIsNil(t *testing.T) {
assert.True(t, util.IsNil(nil))
assert.True(t, util.IsNil((*int)(nil)))
assert.True(t, util.IsNil((chan int)(nil)))
assert.True(t, util.IsNil((map[int]int)(nil)))
assert.True(t, util.IsNil(([]int)(nil)))
assert.True(t, util.IsNil((func())(nil)))
assert.True(t, util.IsNil((interface{})(nil)))
assert.True(t, util.IsNil((error)(nil)))
assert.False(t, util.IsNil(0))
assert.False(t, util.IsNil(""))
assert.False(t, util.IsNil([]int{}))
assert.False(t, util.IsNil(struct{}{}))
}

func TestFilterNil(t *testing.T) {
type testStruct struct {
a, b, c int
}
assert.Equal(t, []interface{}{0, ""}, util.FilterNil([]interface{}{0, nil, ""}))
assert.Equal(t, []*testStruct{{1, 2, 3}, {}}, util.FilterNil([]*testStruct{{1, 2, 3}, nil, {}, (*testStruct)(nil)}))
}
9 changes: 4 additions & 5 deletions internal/cmd/volume/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@ import (

var DeleteCmd = base.DeleteCmd{
ResourceNameSingular: "Volume",
ResourceNamePlural: "volumes",
ShortDescription: "Delete a Volume",
NameSuggestions: func(c hcapi2.Client) func() []string { return c.Volume().Names },
Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) {
return s.Client().Volume().Get(s, idOrName)
},
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) {
volume := resource.(*hcloud.Volume)
if _, err := s.Client().Volume().Delete(s, volume); err != nil {
return err
}
return nil
_, err := s.Client().Volume().Delete(s, volume)
return nil, err
},
}

0 comments on commit 01eac0c

Please sign in to comment.