Browse code

Merge pull request #32110 from adshmh/30977-stack-rm-should-accept-multiple-labels

stack rm should accept multiple arguments

Victor Vieux authored on 2017/04/11 10:19:59
Showing 5 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,153 @@
0
+package stack
1
+
2
+import (
3
+	"strings"
4
+
5
+	"github.com/docker/docker/api/types"
6
+	"github.com/docker/docker/api/types/filters"
7
+	"github.com/docker/docker/api/types/swarm"
8
+	"github.com/docker/docker/cli/compose/convert"
9
+	"github.com/docker/docker/client"
10
+	"golang.org/x/net/context"
11
+)
12
+
13
+type fakeClient struct {
14
+	client.Client
15
+
16
+	services []string
17
+	networks []string
18
+	secrets  []string
19
+
20
+	removedServices []string
21
+	removedNetworks []string
22
+	removedSecrets  []string
23
+
24
+	serviceListFunc   func(options types.ServiceListOptions) ([]swarm.Service, error)
25
+	networkListFunc   func(options types.NetworkListOptions) ([]types.NetworkResource, error)
26
+	secretListFunc    func(options types.SecretListOptions) ([]swarm.Secret, error)
27
+	serviceRemoveFunc func(serviceID string) error
28
+	networkRemoveFunc func(networkID string) error
29
+	secretRemoveFunc  func(secretID string) error
30
+}
31
+
32
+func (cli *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
33
+	if cli.serviceListFunc != nil {
34
+		return cli.serviceListFunc(options)
35
+	}
36
+
37
+	namespace := namespaceFromFilters(options.Filters)
38
+	servicesList := []swarm.Service{}
39
+	for _, name := range cli.services {
40
+		if belongToNamespace(name, namespace) {
41
+			servicesList = append(servicesList, serviceFromName(name))
42
+		}
43
+	}
44
+	return servicesList, nil
45
+}
46
+
47
+func (cli *fakeClient) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
48
+	if cli.networkListFunc != nil {
49
+		return cli.networkListFunc(options)
50
+	}
51
+
52
+	namespace := namespaceFromFilters(options.Filters)
53
+	networksList := []types.NetworkResource{}
54
+	for _, name := range cli.networks {
55
+		if belongToNamespace(name, namespace) {
56
+			networksList = append(networksList, networkFromName(name))
57
+		}
58
+	}
59
+	return networksList, nil
60
+}
61
+
62
+func (cli *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
63
+	if cli.secretListFunc != nil {
64
+		return cli.secretListFunc(options)
65
+	}
66
+
67
+	namespace := namespaceFromFilters(options.Filters)
68
+	secretsList := []swarm.Secret{}
69
+	for _, name := range cli.secrets {
70
+		if belongToNamespace(name, namespace) {
71
+			secretsList = append(secretsList, secretFromName(name))
72
+		}
73
+	}
74
+	return secretsList, nil
75
+}
76
+
77
+func (cli *fakeClient) ServiceRemove(ctx context.Context, serviceID string) error {
78
+	if cli.serviceRemoveFunc != nil {
79
+		return cli.serviceRemoveFunc(serviceID)
80
+	}
81
+
82
+	cli.removedServices = append(cli.removedServices, serviceID)
83
+	return nil
84
+}
85
+
86
+func (cli *fakeClient) NetworkRemove(ctx context.Context, networkID string) error {
87
+	if cli.networkRemoveFunc != nil {
88
+		return cli.networkRemoveFunc(networkID)
89
+	}
90
+
91
+	cli.removedNetworks = append(cli.removedNetworks, networkID)
92
+	return nil
93
+}
94
+
95
+func (cli *fakeClient) SecretRemove(ctx context.Context, secretID string) error {
96
+	if cli.secretRemoveFunc != nil {
97
+		return cli.secretRemoveFunc(secretID)
98
+	}
99
+
100
+	cli.removedSecrets = append(cli.removedSecrets, secretID)
101
+	return nil
102
+}
103
+
104
+func serviceFromName(name string) swarm.Service {
105
+	return swarm.Service{
106
+		ID: "ID-" + name,
107
+		Spec: swarm.ServiceSpec{
108
+			Annotations: swarm.Annotations{Name: name},
109
+		},
110
+	}
111
+}
112
+
113
+func networkFromName(name string) types.NetworkResource {
114
+	return types.NetworkResource{
115
+		ID:   "ID-" + name,
116
+		Name: name,
117
+	}
118
+}
119
+
120
+func secretFromName(name string) swarm.Secret {
121
+	return swarm.Secret{
122
+		ID: "ID-" + name,
123
+		Spec: swarm.SecretSpec{
124
+			Annotations: swarm.Annotations{Name: name},
125
+		},
126
+	}
127
+}
128
+
129
+func namespaceFromFilters(filters filters.Args) string {
130
+	label := filters.Get("label")[0]
131
+	return strings.TrimPrefix(label, convert.LabelNamespace+"=")
132
+}
133
+
134
+func belongToNamespace(id, namespace string) bool {
135
+	return strings.HasPrefix(id, namespace+"_")
136
+}
137
+
138
+func objectName(namespace, name string) string {
139
+	return namespace + "_" + name
140
+}
141
+
142
+func objectID(name string) string {
143
+	return "ID-" + name
144
+}
145
+
146
+func buildObjectIDs(objectNames []string) []string {
147
+	IDs := make([]string, len(objectNames))
148
+	for i, name := range objectNames {
149
+		IDs[i] = objectID(name)
150
+	}
151
+	return IDs
152
+}
... ...
@@ -4,39 +4,12 @@ import (
4 4
 	"bytes"
5 5
 	"testing"
6 6
 
7
-	"github.com/docker/docker/api/types"
8
-	"github.com/docker/docker/api/types/swarm"
9 7
 	"github.com/docker/docker/cli/compose/convert"
10 8
 	"github.com/docker/docker/cli/internal/test"
11
-	"github.com/docker/docker/client"
12 9
 	"github.com/docker/docker/pkg/testutil/assert"
13 10
 	"golang.org/x/net/context"
14 11
 )
15 12
 
16
-type fakeClient struct {
17
-	client.Client
18
-	serviceList []string
19
-	removedIDs  []string
20
-}
21
-
22
-func (cli *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
23
-	services := []swarm.Service{}
24
-	for _, name := range cli.serviceList {
25
-		services = append(services, swarm.Service{
26
-			ID: name,
27
-			Spec: swarm.ServiceSpec{
28
-				Annotations: swarm.Annotations{Name: name},
29
-			},
30
-		})
31
-	}
32
-	return services, nil
33
-}
34
-
35
-func (cli *fakeClient) ServiceRemove(ctx context.Context, serviceID string) error {
36
-	cli.removedIDs = append(cli.removedIDs, serviceID)
37
-	return nil
38
-}
39
-
40 13
 func TestPruneServices(t *testing.T) {
41 14
 	ctx := context.Background()
42 15
 	namespace := convert.NewNamespace("foo")
... ...
@@ -44,11 +17,11 @@ func TestPruneServices(t *testing.T) {
44 44
 		"new":  {},
45 45
 		"keep": {},
46 46
 	}
47
-	client := &fakeClient{serviceList: []string{"foo_keep", "foo_remove"}}
47
+	client := &fakeClient{services: []string{objectName("foo", "keep"), objectName("foo", "remove")}}
48 48
 	dockerCli := test.NewFakeCli(client, &bytes.Buffer{})
49 49
 	dockerCli.SetErr(&bytes.Buffer{})
50 50
 
51 51
 	pruneServices(ctx, dockerCli, namespace, services)
52 52
 
53
-	assert.DeepEqual(t, client.removedIDs, []string{"foo_remove"})
53
+	assert.DeepEqual(t, client.removedServices, buildObjectIDs([]string{objectName("foo", "remove")}))
54 54
 }
... ...
@@ -2,6 +2,7 @@ package stack
2 2
 
3 3
 import (
4 4
 	"fmt"
5
+	"strings"
5 6
 
6 7
 	"github.com/docker/docker/api/types"
7 8
 	"github.com/docker/docker/api/types/swarm"
... ...
@@ -13,56 +14,63 @@ import (
13 13
 )
14 14
 
15 15
 type removeOptions struct {
16
-	namespace string
16
+	namespaces []string
17 17
 }
18 18
 
19
-func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
19
+func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
20 20
 	var opts removeOptions
21 21
 
22 22
 	cmd := &cobra.Command{
23
-		Use:     "rm STACK",
23
+		Use:     "rm STACK [STACK...]",
24 24
 		Aliases: []string{"remove", "down"},
25
-		Short:   "Remove the stack",
26
-		Args:    cli.ExactArgs(1),
25
+		Short:   "Remove one or more stacks",
26
+		Args:    cli.RequiresMinArgs(1),
27 27
 		RunE: func(cmd *cobra.Command, args []string) error {
28
-			opts.namespace = args[0]
28
+			opts.namespaces = args
29 29
 			return runRemove(dockerCli, opts)
30 30
 		},
31 31
 	}
32 32
 	return cmd
33 33
 }
34 34
 
35
-func runRemove(dockerCli *command.DockerCli, opts removeOptions) error {
36
-	namespace := opts.namespace
35
+func runRemove(dockerCli command.Cli, opts removeOptions) error {
36
+	namespaces := opts.namespaces
37 37
 	client := dockerCli.Client()
38 38
 	ctx := context.Background()
39 39
 
40
-	services, err := getServices(ctx, client, namespace)
41
-	if err != nil {
42
-		return err
43
-	}
40
+	var errs []string
41
+	for _, namespace := range namespaces {
42
+		services, err := getServices(ctx, client, namespace)
43
+		if err != nil {
44
+			return err
45
+		}
44 46
 
45
-	networks, err := getStackNetworks(ctx, client, namespace)
46
-	if err != nil {
47
-		return err
48
-	}
47
+		networks, err := getStackNetworks(ctx, client, namespace)
48
+		if err != nil {
49
+			return err
50
+		}
49 51
 
50
-	secrets, err := getStackSecrets(ctx, client, namespace)
51
-	if err != nil {
52
-		return err
53
-	}
52
+		secrets, err := getStackSecrets(ctx, client, namespace)
53
+		if err != nil {
54
+			return err
55
+		}
54 56
 
55
-	if len(services)+len(networks)+len(secrets) == 0 {
56
-		fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace)
57
-		return nil
58
-	}
57
+		if len(services)+len(networks)+len(secrets) == 0 {
58
+			fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace)
59
+			continue
60
+		}
61
+
62
+		hasError := removeServices(ctx, dockerCli, services)
63
+		hasError = removeSecrets(ctx, dockerCli, secrets) || hasError
64
+		hasError = removeNetworks(ctx, dockerCli, networks) || hasError
59 65
 
60
-	hasError := removeServices(ctx, dockerCli, services)
61
-	hasError = removeSecrets(ctx, dockerCli, secrets) || hasError
62
-	hasError = removeNetworks(ctx, dockerCli, networks) || hasError
66
+		if hasError {
67
+			errs = append(errs, fmt.Sprintf("Failed to remove some resources from stack: %s", namespace))
68
+		}
69
+	}
63 70
 
64
-	if hasError {
65
-		return errors.Errorf("Failed to remove some resources")
71
+	if len(errs) > 0 {
72
+		return errors.Errorf(strings.Join(errs, "\n"))
66 73
 	}
67 74
 	return nil
68 75
 }
69 76
new file mode 100644
... ...
@@ -0,0 +1,107 @@
0
+package stack
1
+
2
+import (
3
+	"bytes"
4
+	"errors"
5
+	"strings"
6
+	"testing"
7
+
8
+	"github.com/docker/docker/cli/internal/test"
9
+	"github.com/docker/docker/pkg/testutil/assert"
10
+)
11
+
12
+func TestRemoveStack(t *testing.T) {
13
+	allServices := []string{
14
+		objectName("foo", "service1"),
15
+		objectName("foo", "service2"),
16
+		objectName("bar", "service1"),
17
+		objectName("bar", "service2"),
18
+	}
19
+	allServicesIDs := buildObjectIDs(allServices)
20
+
21
+	allNetworks := []string{
22
+		objectName("foo", "network1"),
23
+		objectName("bar", "network1"),
24
+	}
25
+	allNetworksIDs := buildObjectIDs(allNetworks)
26
+
27
+	allSecrets := []string{
28
+		objectName("foo", "secret1"),
29
+		objectName("foo", "secret2"),
30
+		objectName("bar", "secret1"),
31
+	}
32
+	allSecretsIDs := buildObjectIDs(allSecrets)
33
+
34
+	cli := &fakeClient{
35
+		services: allServices,
36
+		networks: allNetworks,
37
+		secrets:  allSecrets,
38
+	}
39
+	cmd := newRemoveCommand(test.NewFakeCli(cli, &bytes.Buffer{}))
40
+	cmd.SetArgs([]string{"foo", "bar"})
41
+
42
+	assert.NilError(t, cmd.Execute())
43
+	assert.DeepEqual(t, cli.removedServices, allServicesIDs)
44
+	assert.DeepEqual(t, cli.removedNetworks, allNetworksIDs)
45
+	assert.DeepEqual(t, cli.removedSecrets, allSecretsIDs)
46
+}
47
+
48
+func TestSkipEmptyStack(t *testing.T) {
49
+	buf := new(bytes.Buffer)
50
+	allServices := []string{objectName("bar", "service1"), objectName("bar", "service2")}
51
+	allServicesIDs := buildObjectIDs(allServices)
52
+
53
+	allNetworks := []string{objectName("bar", "network1")}
54
+	allNetworksIDs := buildObjectIDs(allNetworks)
55
+
56
+	allSecrets := []string{objectName("bar", "secret1")}
57
+	allSecretsIDs := buildObjectIDs(allSecrets)
58
+
59
+	cli := &fakeClient{
60
+		services: allServices,
61
+		networks: allNetworks,
62
+		secrets:  allSecrets,
63
+	}
64
+	cmd := newRemoveCommand(test.NewFakeCli(cli, buf))
65
+	cmd.SetArgs([]string{"foo", "bar"})
66
+
67
+	assert.NilError(t, cmd.Execute())
68
+	assert.Contains(t, buf.String(), "Nothing found in stack: foo")
69
+	assert.DeepEqual(t, cli.removedServices, allServicesIDs)
70
+	assert.DeepEqual(t, cli.removedNetworks, allNetworksIDs)
71
+	assert.DeepEqual(t, cli.removedSecrets, allSecretsIDs)
72
+}
73
+
74
+func TestContinueAfterError(t *testing.T) {
75
+	allServices := []string{objectName("foo", "service1"), objectName("bar", "service1")}
76
+	allServicesIDs := buildObjectIDs(allServices)
77
+
78
+	allNetworks := []string{objectName("foo", "network1"), objectName("bar", "network1")}
79
+	allNetworksIDs := buildObjectIDs(allNetworks)
80
+
81
+	allSecrets := []string{objectName("foo", "secret1"), objectName("bar", "secret1")}
82
+	allSecretsIDs := buildObjectIDs(allSecrets)
83
+
84
+	removedServices := []string{}
85
+	cli := &fakeClient{
86
+		services: allServices,
87
+		networks: allNetworks,
88
+		secrets:  allSecrets,
89
+
90
+		serviceRemoveFunc: func(serviceID string) error {
91
+			removedServices = append(removedServices, serviceID)
92
+
93
+			if strings.Contains(serviceID, "foo") {
94
+				return errors.New("")
95
+			}
96
+			return nil
97
+		},
98
+	}
99
+	cmd := newRemoveCommand(test.NewFakeCli(cli, &bytes.Buffer{}))
100
+	cmd.SetArgs([]string{"foo", "bar"})
101
+
102
+	assert.Error(t, cmd.Execute(), "Failed to remove some resources from stack: foo")
103
+	assert.DeepEqual(t, removedServices, allServicesIDs)
104
+	assert.DeepEqual(t, cli.removedNetworks, allNetworksIDs)
105
+	assert.DeepEqual(t, cli.removedSecrets, allSecretsIDs)
106
+}
... ...
@@ -16,9 +16,9 @@ keywords: "stack, rm, remove, down"
16 16
 # stack rm
17 17
 
18 18
 ```markdown
19
-Usage:  docker stack rm STACK
19
+Usage:  docker stack rm STACK [STACK...]
20 20
 
21
-Remove the stack
21
+Remove one or more stacks
22 22
 
23 23
 Aliases:
24 24
   rm, remove, down
... ...
@@ -32,6 +32,44 @@ Options:
32 32
 Remove the stack from the swarm. This command has to be run targeting
33 33
 a manager node.
34 34
 
35
+## Examples
36
+
37
+### Remove a stack
38
+
39
+This will remove the stack with the name `myapp`. Services, networks, and secrets associated with the stack will be removed.
40
+
41
+```bash
42
+$ docker stack rm myapp
43
+
44
+Removing service myapp_redis
45
+Removing service myapp_web
46
+Removing service myapp_lb
47
+Removing network myapp_default
48
+Removing network myapp_frontend
49
+```
50
+
51
+### Remove multiple stacks
52
+
53
+This will remove all the specified stacks, `myapp` and `vossibility`. Services, networks, and secrets associated with all the specified stacks will be removed.
54
+
55
+```bash
56
+$ docker stack rm myapp vossibility
57
+
58
+Removing service myapp_redis
59
+Removing service myapp_web
60
+Removing service myapp_lb
61
+Removing network myapp_default
62
+Removing network myapp_frontend
63
+Removing service vossibility_nsqd
64
+Removing service vossibility_logstash
65
+Removing service vossibility_elasticsearch
66
+Removing service vossibility_kibana
67
+Removing service vossibility_ghollector
68
+Removing service vossibility_lookupd
69
+Removing network vossibility_default
70
+Removing network vossibility_vossibility
71
+```
72
+
35 73
 ## Related commands
36 74
 
37 75
 * [stack deploy](stack_deploy.md)