diff --git a/internal/cmd/base/delete.go b/internal/cmd/base/delete.go index 150d260a..d98032e7 100644 --- a/internal/cmd/base/delete.go +++ b/internal/cmd/base/delete.go @@ -3,7 +3,8 @@ package base import ( "errors" "fmt" - "reflect" + "strings" + "sync" "github.com/spf13/cobra" @@ -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. @@ -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)...) } diff --git a/internal/cmd/base/delete_test.go b/internal/cmd/base/delete_test.go index eff0bcfb..6f33f0da 100644 --- a/internal/cmd/base/delete_test.go +++ b/internal/cmd/base/delete_test.go @@ -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 { cmd.Println("Deleting fake resource") return nil diff --git a/internal/cmd/certificate/delete.go b/internal/cmd/certificate/delete.go index 66f51813..ce9f8209 100644 --- a/internal/cmd/certificate/delete.go +++ b/internal/cmd/certificate/delete.go @@ -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 }, } diff --git a/internal/cmd/firewall/delete.go b/internal/cmd/firewall/delete.go index eafce2ba..adda6827 100644 --- a/internal/cmd/firewall/delete.go +++ b/internal/cmd/firewall/delete.go @@ -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 }, } diff --git a/internal/cmd/floatingip/delete.go b/internal/cmd/floatingip/delete.go index 010b603f..3ae3ffba 100644 --- a/internal/cmd/floatingip/delete.go +++ b/internal/cmd/floatingip/delete.go @@ -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 }, } diff --git a/internal/cmd/image/delete.go b/internal/cmd/image/delete.go index 1f7f8cd9..77066e76 100644 --- a/internal/cmd/image/delete.go +++ b/internal/cmd/image/delete.go @@ -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 }, } diff --git a/internal/cmd/loadbalancer/delete.go b/internal/cmd/loadbalancer/delete.go index de5a1816..83172f54 100644 --- a/internal/cmd/loadbalancer/delete.go +++ b/internal/cmd/loadbalancer/delete.go @@ -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 }, } diff --git a/internal/cmd/network/delete.go b/internal/cmd/network/delete.go index 1507ab1b..39531a1d 100644 --- a/internal/cmd/network/delete.go +++ b/internal/cmd/network/delete.go @@ -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 }, } diff --git a/internal/cmd/placementgroup/delete.go b/internal/cmd/placementgroup/delete.go index 13b52512..87e34de1 100644 --- a/internal/cmd/placementgroup/delete.go +++ b/internal/cmd/placementgroup/delete.go @@ -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 }, } diff --git a/internal/cmd/primaryip/delete.go b/internal/cmd/primaryip/delete.go index 87caf3c8..56adc48e 100644 --- a/internal/cmd/primaryip/delete.go +++ b/internal/cmd/primaryip/delete.go @@ -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 }, } diff --git a/internal/cmd/server/delete.go b/internal/cmd/server/delete.go index 11bd998e..d16adbb5 100644 --- a/internal/cmd/server/delete.go +++ b/internal/cmd/server/delete.go @@ -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 }, } diff --git a/internal/cmd/sshkey/delete.go b/internal/cmd/sshkey/delete.go index 902be564..67f2b43d 100644 --- a/internal/cmd/sshkey/delete.go +++ b/internal/cmd/sshkey/delete.go @@ -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 }, } diff --git a/internal/cmd/util/util.go b/internal/cmd/util/util.go index 05c8301e..e48f79c7 100644 --- a/internal/cmd/util/util.go +++ b/internal/cmd/util/util.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "reflect" "sort" "strings" "text/template" @@ -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 +} diff --git a/internal/cmd/util/util_test.go b/internal/cmd/util/util_test.go index a90e0ba1..dd109af6 100644 --- a/internal/cmd/util/util_test.go +++ b/internal/cmd/util/util_test.go @@ -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)})) +} diff --git a/internal/cmd/volume/delete.go b/internal/cmd/volume/delete.go index 9aa9b1c3..39deae25 100644 --- a/internal/cmd/volume/delete.go +++ b/internal/cmd/volume/delete.go @@ -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 }, }