Skip to content

Commit f2fb321

Browse files
authored
feat: delete multiple resources in parallel (#761)
This PR builds onto #719 by making the deletion of multiple resources parallel using the new action waiters introduced in #749.
1 parent 58279a3 commit f2fb321

26 files changed

Lines changed: 161 additions & 162 deletions

internal/cmd/base/delete.go

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ package base
33
import (
44
"errors"
55
"fmt"
6-
"reflect"
6+
"strings"
7+
"sync"
78

89
"github.com/spf13/cobra"
910

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

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

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

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

63-
// resource is an interface that always has a type, so the interface is never nil
64-
// (i.e. == nil) is always false.
65-
if reflect.ValueOf(resource).IsNil() {
66-
cmdErr = errors.Join(cmdErr, fmt.Errorf("%s not found: %s", dc.ResourceNameSingular, idOrName))
67-
continue
68-
}
63+
for i, idOrName := range args {
64+
i, idOrName := i, idOrName
65+
go func() {
66+
defer wg.Done()
67+
resource, _, err := dc.Fetch(s, cmd, idOrName)
68+
if err != nil {
69+
errs[i] = err
70+
return
71+
}
72+
if util.IsNil(resource) {
73+
errs[i] = fmt.Errorf("%s not found: %s", dc.ResourceNameSingular, idOrName)
74+
return
75+
}
76+
actions[i], errs[i] = dc.Delete(s, cmd, resource)
77+
}()
78+
}
6979

70-
if err = dc.Delete(s, cmd, resource); err != nil {
71-
cmdErr = errors.Join(cmdErr, fmt.Errorf("deleting %s %s failed: %s", dc.ResourceNameSingular, idOrName, err))
80+
wg.Wait()
81+
filtered := util.FilterNil(actions)
82+
var err error
83+
if len(filtered) > 0 {
84+
err = s.WaitForActions(cmd, s, filtered...)
85+
}
86+
87+
var actuallyDeleted []string
88+
for i, idOrName := range args {
89+
if errs[i] == nil {
90+
actuallyDeleted = append(actuallyDeleted, idOrName)
7291
}
73-
cmd.Printf("%s %v deleted\n", dc.ResourceNameSingular, idOrName)
7492
}
7593

76-
return cmdErr
94+
if len(actuallyDeleted) == 1 {
95+
cmd.Printf("%s %s deleted\n", dc.ResourceNameSingular, actuallyDeleted[0])
96+
} else if len(actuallyDeleted) > 1 {
97+
cmd.Printf("%s %s deleted\n", dc.ResourceNamePlural, strings.Join(actuallyDeleted, ", "))
98+
}
99+
return errors.Join(append(errs, err)...)
77100
}

internal/cmd/base/delete_test.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package base_test
22

33
import (
4+
"sync"
45
"testing"
56

67
"github.com/spf13/cobra"
@@ -12,14 +13,19 @@ import (
1213
"github.com/hetznercloud/hcloud-go/v2/hcloud"
1314
)
1415

16+
var mu = sync.Mutex{}
17+
1518
var fakeDeleteCmd = &base.DeleteCmd{
1619
ResourceNameSingular: "Fake resource",
17-
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {
20+
ResourceNamePlural: "Fake resources",
21+
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) {
22+
defer mu.Unlock()
1823
cmd.Println("Deleting fake resource")
19-
return nil
24+
return nil, nil
2025
},
2126

2227
Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) {
28+
mu.Lock()
2329
cmd.Println("Fetching fake resource")
2430

2531
resource := &fakeResource{
@@ -43,8 +49,8 @@ func TestDelete(t *testing.T) {
4349
},
4450
"no flags multiple": {
4551
Args: []string{"delete", "123", "456", "789"},
46-
ExpOut: "Fetching fake resource\nDeleting fake resource\nFake resource 123 deleted\nFetching fake resource\n" +
47-
"Deleting fake resource\nFake resource 456 deleted\nFetching fake resource\nDeleting fake resource\nFake resource 789 deleted\n",
52+
ExpOut: "Fetching fake resource\nDeleting fake resource\nFetching fake resource\nDeleting fake resource\n" +
53+
"Fetching fake resource\nDeleting fake resource\nFake resources 123, 456, 789 deleted\n",
4854
},
4955
"quiet": {
5056
Args: []string{"delete", "123", "--quiet"},

internal/cmd/certificate/delete.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,15 @@ import (
1111

1212
var DeleteCmd = base.DeleteCmd{
1313
ResourceNameSingular: "certificate",
14+
ResourceNamePlural: "certificates",
1415
ShortDescription: "Delete a certificate",
1516
NameSuggestions: func(c hcapi2.Client) func() []string { return c.Firewall().Names },
1617
Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) {
1718
return s.Client().Certificate().Get(s, idOrName)
1819
},
19-
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {
20+
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) {
2021
certificate := resource.(*hcloud.Certificate)
21-
if _, err := s.Client().Certificate().Delete(s, certificate); err != nil {
22-
return err
23-
}
24-
return nil
22+
_, err := s.Client().Certificate().Delete(s, certificate)
23+
return nil, err
2524
},
2625
}

internal/cmd/certificate/delete_test.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package certificate_test
22

33
import (
4-
"fmt"
5-
"strings"
64
"testing"
75

86
"github.com/golang/mock/gomock"
@@ -63,12 +61,9 @@ func TestDeleteMultiple(t *testing.T) {
6361
},
6462
}
6563

66-
expOutBuilder := strings.Builder{}
67-
6864
var names []string
6965
for _, cert := range certs {
7066
names = append(names, cert.Name)
71-
expOutBuilder.WriteString(fmt.Sprintf("certificate %s deleted\n", cert.Name))
7267
fx.Client.CertificateClient.EXPECT().
7368
Get(gomock.Any(), cert.Name).
7469
Return(cert, nil, nil)
@@ -78,9 +73,8 @@ func TestDeleteMultiple(t *testing.T) {
7873
}
7974

8075
out, errOut, err := fx.Run(cmd, names)
81-
expOut := expOutBuilder.String()
8276

8377
assert.NoError(t, err)
8478
assert.Empty(t, errOut)
85-
assert.Equal(t, expOut, out)
79+
assert.Equal(t, "certificates test1, test2, test3 deleted\n", out)
8680
}

internal/cmd/firewall/delete.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,15 @@ import (
1111

1212
var DeleteCmd = base.DeleteCmd{
1313
ResourceNameSingular: "firewall",
14+
ResourceNamePlural: "firewalls",
1415
ShortDescription: "Delete a firewall",
1516
NameSuggestions: func(c hcapi2.Client) func() []string { return c.Firewall().Names },
1617
Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) {
1718
return s.Client().Firewall().Get(s, idOrName)
1819
},
19-
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {
20+
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) {
2021
firewall := resource.(*hcloud.Firewall)
21-
if _, err := s.Client().Firewall().Delete(s, firewall); err != nil {
22-
return err
23-
}
24-
return nil
22+
_, err := s.Client().Firewall().Delete(s, firewall)
23+
return nil, err
2524
},
2625
}

internal/cmd/firewall/delete_test.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package firewall_test
22

33
import (
4-
"fmt"
5-
"strings"
64
"testing"
75

86
"github.com/golang/mock/gomock"
@@ -63,12 +61,9 @@ func TestDeleteMultiple(t *testing.T) {
6361
},
6462
}
6563

66-
expOutBuilder := strings.Builder{}
67-
6864
var names []string
6965
for _, fw := range firewalls {
7066
names = append(names, fw.Name)
71-
expOutBuilder.WriteString(fmt.Sprintf("firewall %s deleted\n", fw.Name))
7267
fx.Client.FirewallClient.EXPECT().
7368
Get(gomock.Any(), fw.Name).
7469
Return(fw, nil, nil)
@@ -78,9 +73,8 @@ func TestDeleteMultiple(t *testing.T) {
7873
}
7974

8075
out, errOut, err := fx.Run(cmd, names)
81-
expOut := expOutBuilder.String()
8276

8377
assert.NoError(t, err)
8478
assert.Empty(t, errOut)
85-
assert.Equal(t, expOut, out)
79+
assert.Equal(t, "firewalls test1, test2, test3 deleted\n", out)
8680
}

internal/cmd/floatingip/delete.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,15 @@ import (
1111

1212
var DeleteCmd = base.DeleteCmd{
1313
ResourceNameSingular: "Floating IP",
14+
ResourceNamePlural: "Floating IPs",
1415
ShortDescription: "Delete a Floating IP",
1516
NameSuggestions: func(c hcapi2.Client) func() []string { return c.FloatingIP().Names },
1617
Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) {
1718
return s.Client().FloatingIP().Get(s, idOrName)
1819
},
19-
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {
20+
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) {
2021
floatingIP := resource.(*hcloud.FloatingIP)
21-
if _, err := s.Client().FloatingIP().Delete(s, floatingIP); err != nil {
22-
return err
23-
}
24-
return nil
22+
_, err := s.Client().FloatingIP().Delete(s, floatingIP)
23+
return nil, err
2524
},
2625
}

internal/cmd/floatingip/delete_test.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package floatingip_test
22

33
import (
4-
"fmt"
5-
"strings"
64
"testing"
75

86
"github.com/golang/mock/gomock"
@@ -63,12 +61,9 @@ func TestDeleteMultiple(t *testing.T) {
6361
},
6462
}
6563

66-
expOutBuilder := strings.Builder{}
67-
6864
var names []string
6965
for _, ip := range ips {
7066
names = append(names, ip.Name)
71-
expOutBuilder.WriteString(fmt.Sprintf("Floating IP %s deleted\n", ip.Name))
7267
fx.Client.FloatingIPClient.EXPECT().
7368
Get(gomock.Any(), ip.Name).
7469
Return(ip, nil, nil)
@@ -78,9 +73,8 @@ func TestDeleteMultiple(t *testing.T) {
7873
}
7974

8075
out, errOut, err := fx.Run(cmd, names)
81-
expOut := expOutBuilder.String()
8276

8377
assert.NoError(t, err)
8478
assert.Empty(t, errOut)
85-
assert.Equal(t, expOut, out)
79+
assert.Equal(t, "Floating IPs test1, test2, test3 deleted\n", out)
8680
}

internal/cmd/image/delete.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,15 @@ import (
1111

1212
var DeleteCmd = base.DeleteCmd{
1313
ResourceNameSingular: "image",
14+
ResourceNamePlural: "images",
1415
ShortDescription: "Delete an image",
1516
NameSuggestions: func(c hcapi2.Client) func() []string { return c.Image().Names },
1617
Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, *hcloud.Response, error) {
1718
return s.Client().Image().Get(s, idOrName)
1819
},
19-
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) error {
20+
Delete: func(s state.State, cmd *cobra.Command, resource interface{}) (*hcloud.Action, error) {
2021
image := resource.(*hcloud.Image)
21-
if _, err := s.Client().Image().Delete(s, image); err != nil {
22-
return err
23-
}
24-
return nil
22+
_, err := s.Client().Image().Delete(s, image)
23+
return nil, err
2524
},
2625
}

internal/cmd/image/delete_test.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package image_test
22

33
import (
4-
"fmt"
5-
"strings"
64
"testing"
75

86
"github.com/golang/mock/gomock"
@@ -63,12 +61,9 @@ func TestDeleteMultiple(t *testing.T) {
6361
},
6462
}
6563

66-
expOutBuilder := strings.Builder{}
67-
6864
var names []string
6965
for _, img := range images {
7066
names = append(names, img.Name)
71-
expOutBuilder.WriteString(fmt.Sprintf("image %s deleted\n", img.Name))
7267
fx.Client.ImageClient.EXPECT().
7368
Get(gomock.Any(), img.Name).
7469
Return(img, nil, nil)
@@ -78,9 +73,8 @@ func TestDeleteMultiple(t *testing.T) {
7873
}
7974

8075
out, errOut, err := fx.Run(cmd, names)
81-
expOut := expOutBuilder.String()
8276

8377
assert.NoError(t, err)
8478
assert.Empty(t, errOut)
85-
assert.Equal(t, expOut, out)
79+
assert.Equal(t, "images test1, test2, test3 deleted\n", out)
8680
}

0 commit comments

Comments
 (0)