diff --git a/internal/cmd/base/delete.go b/internal/cmd/base/delete.go index 150d260a..0653a10c 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, filtered...) + } + + 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..1b2194c4 100644 --- a/internal/cmd/base/delete_test.go +++ b/internal/cmd/base/delete_test.go @@ -1,6 +1,7 @@ package base_test import ( + "sync" "testing" "github.com/spf13/cobra" @@ -12,14 +13,19 @@ import ( "github.com/hetznercloud/hcloud-go/v2/hcloud" ) +var mu = sync.Mutex{} + var fakeDeleteCmd = &base.DeleteCmd{ ResourceNameSingular: "Fake resource", - Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error { + ResourceNamePlural: "Fake resources", + Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) { + defer mu.Unlock() cmd.Println("Deleting fake resource") - return nil + return nil, nil }, Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) { + mu.Lock() cmd.Println("Fetching fake resource") resource := &fakeResource{ @@ -43,8 +49,8 @@ func TestDelete(t *testing.T) { }, "no flags multiple": { Args: []string{"delete", "123", "456", "789"}, - ExpOut: "Fetching fake resource\nDeleting fake resource\nFake resource 123 deleted\nFetching fake resource\n" + - "Deleting fake resource\nFake resource 456 deleted\nFetching fake resource\nDeleting fake resource\nFake resource 789 deleted\n", + ExpOut: "Fetching fake resource\nDeleting fake resource\nFetching fake resource\nDeleting fake resource\n" + + "Fetching fake resource\nDeleting fake resource\nFake resources 123, 456, 789 deleted\n", }, "quiet": { Args: []string{"delete", "123", "--quiet"}, 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/certificate/delete_test.go b/internal/cmd/certificate/delete_test.go index 4fe0056b..b48a2cc0 100644 --- a/internal/cmd/certificate/delete_test.go +++ b/internal/cmd/certificate/delete_test.go @@ -1,8 +1,6 @@ package certificate_test import ( - "fmt" - "strings" "testing" "github.com/golang/mock/gomock" @@ -63,12 +61,9 @@ func TestDeleteMultiple(t *testing.T) { }, } - expOutBuilder := strings.Builder{} - var names []string for _, cert := range certs { names = append(names, cert.Name) - expOutBuilder.WriteString(fmt.Sprintf("certificate %s deleted\n", cert.Name)) fx.Client.CertificateClient.EXPECT(). Get(gomock.Any(), cert.Name). Return(cert, nil, nil) @@ -78,9 +73,8 @@ func TestDeleteMultiple(t *testing.T) { } out, errOut, err := fx.Run(cmd, names) - expOut := expOutBuilder.String() assert.NoError(t, err) assert.Empty(t, errOut) - assert.Equal(t, expOut, out) + assert.Equal(t, "certificates test1, test2, test3 deleted\n", out) } 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/firewall/delete_test.go b/internal/cmd/firewall/delete_test.go index b78450d4..f74c2903 100644 --- a/internal/cmd/firewall/delete_test.go +++ b/internal/cmd/firewall/delete_test.go @@ -1,8 +1,6 @@ package firewall_test import ( - "fmt" - "strings" "testing" "github.com/golang/mock/gomock" @@ -63,12 +61,9 @@ func TestDeleteMultiple(t *testing.T) { }, } - expOutBuilder := strings.Builder{} - var names []string for _, fw := range firewalls { names = append(names, fw.Name) - expOutBuilder.WriteString(fmt.Sprintf("firewall %s deleted\n", fw.Name)) fx.Client.FirewallClient.EXPECT(). Get(gomock.Any(), fw.Name). Return(fw, nil, nil) @@ -78,9 +73,8 @@ func TestDeleteMultiple(t *testing.T) { } out, errOut, err := fx.Run(cmd, names) - expOut := expOutBuilder.String() assert.NoError(t, err) assert.Empty(t, errOut) - assert.Equal(t, expOut, out) + assert.Equal(t, "firewalls test1, test2, test3 deleted\n", out) } 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/floatingip/delete_test.go b/internal/cmd/floatingip/delete_test.go index afca2dcf..c8b84746 100644 --- a/internal/cmd/floatingip/delete_test.go +++ b/internal/cmd/floatingip/delete_test.go @@ -1,8 +1,6 @@ package floatingip_test import ( - "fmt" - "strings" "testing" "github.com/golang/mock/gomock" @@ -63,12 +61,9 @@ func TestDeleteMultiple(t *testing.T) { }, } - expOutBuilder := strings.Builder{} - var names []string for _, ip := range ips { names = append(names, ip.Name) - expOutBuilder.WriteString(fmt.Sprintf("Floating IP %s deleted\n", ip.Name)) fx.Client.FloatingIPClient.EXPECT(). Get(gomock.Any(), ip.Name). Return(ip, nil, nil) @@ -78,9 +73,8 @@ func TestDeleteMultiple(t *testing.T) { } out, errOut, err := fx.Run(cmd, names) - expOut := expOutBuilder.String() assert.NoError(t, err) assert.Empty(t, errOut) - assert.Equal(t, expOut, out) + assert.Equal(t, "Floating IPs test1, test2, test3 deleted\n", out) } 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/image/delete_test.go b/internal/cmd/image/delete_test.go index 70c954e3..59d0a1e7 100644 --- a/internal/cmd/image/delete_test.go +++ b/internal/cmd/image/delete_test.go @@ -1,8 +1,6 @@ package image_test import ( - "fmt" - "strings" "testing" "github.com/golang/mock/gomock" @@ -63,12 +61,9 @@ func TestDeleteMultiple(t *testing.T) { }, } - expOutBuilder := strings.Builder{} - var names []string for _, img := range images { names = append(names, img.Name) - expOutBuilder.WriteString(fmt.Sprintf("image %s deleted\n", img.Name)) fx.Client.ImageClient.EXPECT(). Get(gomock.Any(), img.Name). Return(img, nil, nil) @@ -78,9 +73,8 @@ func TestDeleteMultiple(t *testing.T) { } out, errOut, err := fx.Run(cmd, names) - expOut := expOutBuilder.String() assert.NoError(t, err) assert.Empty(t, errOut) - assert.Equal(t, expOut, out) + assert.Equal(t, "images test1, test2, test3 deleted\n", out) } 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/loadbalancer/delete_test.go b/internal/cmd/loadbalancer/delete_test.go index a770b30f..07695a4b 100644 --- a/internal/cmd/loadbalancer/delete_test.go +++ b/internal/cmd/loadbalancer/delete_test.go @@ -1,8 +1,6 @@ package loadbalancer_test import ( - "fmt" - "strings" "testing" "github.com/golang/mock/gomock" @@ -63,12 +61,9 @@ func TestDeleteMultiple(t *testing.T) { }, } - expOutBuilder := strings.Builder{} - var names []string for _, lb := range loadBalancers { names = append(names, lb.Name) - expOutBuilder.WriteString(fmt.Sprintf("Load Balancer %s deleted\n", lb.Name)) fx.Client.LoadBalancerClient.EXPECT(). Get(gomock.Any(), lb.Name). Return(lb, nil, nil) @@ -78,9 +73,8 @@ func TestDeleteMultiple(t *testing.T) { } out, errOut, err := fx.Run(cmd, names) - expOut := expOutBuilder.String() assert.NoError(t, err) assert.Empty(t, errOut) - assert.Equal(t, expOut, out) + assert.Equal(t, "Load Balancers test1, test2, test3 deleted\n", out) } 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/network/delete_test.go b/internal/cmd/network/delete_test.go index 39dba670..a563538c 100644 --- a/internal/cmd/network/delete_test.go +++ b/internal/cmd/network/delete_test.go @@ -1,8 +1,6 @@ package network_test import ( - "fmt" - "strings" "testing" "github.com/golang/mock/gomock" @@ -63,12 +61,9 @@ func TestDeleteMultiple(t *testing.T) { }, } - expOutBuilder := strings.Builder{} - var names []string for _, net := range networks { names = append(names, net.Name) - expOutBuilder.WriteString(fmt.Sprintf("Network %s deleted\n", net.Name)) fx.Client.NetworkClient.EXPECT(). Get(gomock.Any(), net.Name). Return(net, nil, nil) @@ -78,9 +73,8 @@ func TestDeleteMultiple(t *testing.T) { } out, errOut, err := fx.Run(cmd, names) - expOut := expOutBuilder.String() assert.NoError(t, err) assert.Empty(t, errOut) - assert.Equal(t, expOut, out) + assert.Equal(t, "Networks test1, test2, test3 deleted\n", out) } 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/placementgroup/delete_test.go b/internal/cmd/placementgroup/delete_test.go index ee248b56..647fbb73 100644 --- a/internal/cmd/placementgroup/delete_test.go +++ b/internal/cmd/placementgroup/delete_test.go @@ -1,8 +1,6 @@ package placementgroup_test import ( - "fmt" - "strings" "testing" "time" @@ -67,12 +65,9 @@ func TestDeleteMultiple(t *testing.T) { }, } - expOutBuilder := strings.Builder{} - var names []string for _, pg := range groups { names = append(names, pg.Name) - expOutBuilder.WriteString(fmt.Sprintf("placement group %s deleted\n", pg.Name)) fx.Client.PlacementGroupClient.EXPECT(). Get(gomock.Any(), pg.Name). Return(pg, nil, nil) @@ -82,9 +77,8 @@ func TestDeleteMultiple(t *testing.T) { } out, errOut, err := fx.Run(cmd, names) - expOut := expOutBuilder.String() assert.NoError(t, err) assert.Empty(t, errOut) - assert.Equal(t, expOut, out) + assert.Equal(t, "placement groups test1, test2, test3 deleted\n", out) } 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/primaryip/delete_test.go b/internal/cmd/primaryip/delete_test.go index c7aa31d0..ee19d549 100644 --- a/internal/cmd/primaryip/delete_test.go +++ b/internal/cmd/primaryip/delete_test.go @@ -1,8 +1,6 @@ package primaryip_test import ( - "fmt" - "strings" "testing" "github.com/golang/mock/gomock" @@ -71,12 +69,9 @@ func TestDeleteMultiple(t *testing.T) { }, } - expOutBuilder := strings.Builder{} - var names []string for _, ip := range ips { names = append(names, ip.Name) - expOutBuilder.WriteString(fmt.Sprintf("Primary IP %s deleted\n", ip.Name)) fx.Client.PrimaryIPClient.EXPECT(). Get(gomock.Any(), ip.Name). Return(ip, nil, nil) @@ -86,9 +81,8 @@ func TestDeleteMultiple(t *testing.T) { } out, errOut, err := fx.Run(cmd, names) - expOut := expOutBuilder.String() assert.NoError(t, err) assert.Empty(t, errOut) - assert.Equal(t, expOut, out) + assert.Equal(t, "Primary IPs test1, test2, test3 deleted\n", out) } 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/server/delete_test.go b/internal/cmd/server/delete_test.go index 21349098..f24eed80 100644 --- a/internal/cmd/server/delete_test.go +++ b/internal/cmd/server/delete_test.go @@ -1,8 +1,6 @@ package server_test import ( - "fmt" - "strings" "testing" "github.com/golang/mock/gomock" @@ -67,12 +65,12 @@ func TestDeleteMultiple(t *testing.T) { }, } - expOutBuilder := strings.Builder{} - - var names []string + var ( + names []string + actions []*hcloud.Action + ) for i, srv := range servers { names = append(names, srv.Name) - expOutBuilder.WriteString(fmt.Sprintf("Server %s deleted\n", srv.Name)) fx.Client.ServerClient.EXPECT(). Get(gomock.Any(), srv.Name). Return(srv, nil, nil) @@ -81,14 +79,13 @@ func TestDeleteMultiple(t *testing.T) { Return(&hcloud.ServerDeleteResult{ Action: &hcloud.Action{ID: int64(i)}, }, nil, nil) - fx.ActionWaiter.EXPECT(). - WaitForActions(gomock.Any(), gomock.Any(), &hcloud.Action{ID: int64(i)}) + actions = append(actions, &hcloud.Action{ID: int64(i)}) } + fx.ActionWaiter.EXPECT().WaitForActions(gomock.Any(), gomock.Any(), actions) out, errOut, err := fx.Run(cmd, names) - expOut := expOutBuilder.String() assert.NoError(t, err) assert.Empty(t, errOut) - assert.Equal(t, expOut, out) + assert.Equal(t, "Servers test1, test2, test3 deleted\n", out) } 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/sshkey/delete_test.go b/internal/cmd/sshkey/delete_test.go index 0c557bcf..a5747e9c 100644 --- a/internal/cmd/sshkey/delete_test.go +++ b/internal/cmd/sshkey/delete_test.go @@ -1,8 +1,6 @@ package sshkey_test import ( - "fmt" - "strings" "testing" "github.com/golang/mock/gomock" @@ -63,12 +61,9 @@ func TestDeleteMultiple(t *testing.T) { }, } - expOutBuilder := strings.Builder{} - var names []string for _, key := range keys { names = append(names, key.Name) - expOutBuilder.WriteString(fmt.Sprintf("SSH Key %s deleted\n", key.Name)) fx.Client.SSHKeyClient.EXPECT(). Get(gomock.Any(), key.Name). Return(key, nil, nil) @@ -78,9 +73,8 @@ func TestDeleteMultiple(t *testing.T) { } out, errOut, err := fx.Run(cmd, names) - expOut := expOutBuilder.String() assert.NoError(t, err) assert.Empty(t, errOut) - assert.Equal(t, expOut, out) + assert.Equal(t, "SSH Keys test1, test2, test3 deleted\n", out) } 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..45b7f96c 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 //nolint:unused + } + 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..41deea2e 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 }, } diff --git a/internal/cmd/volume/delete_test.go b/internal/cmd/volume/delete_test.go index 30ffdffd..453a5ab4 100644 --- a/internal/cmd/volume/delete_test.go +++ b/internal/cmd/volume/delete_test.go @@ -1,8 +1,6 @@ package volume_test import ( - "fmt" - "strings" "testing" "github.com/golang/mock/gomock" @@ -63,12 +61,9 @@ func TestDeleteMultiple(t *testing.T) { }, } - expOutBuilder := strings.Builder{} - var names []string for _, v := range volumes { names = append(names, v.Name) - expOutBuilder.WriteString(fmt.Sprintf("Volume %s deleted\n", v.Name)) fx.Client.VolumeClient.EXPECT(). Get(gomock.Any(), v.Name). Return(v, nil, nil) @@ -78,9 +73,8 @@ func TestDeleteMultiple(t *testing.T) { } out, errOut, err := fx.Run(cmd, names) - expOut := expOutBuilder.String() assert.NoError(t, err) assert.Empty(t, errOut) - assert.Equal(t, expOut, out) + assert.Equal(t, "Volumes test1, test2, test3 deleted\n", out) }