Browse Source

stack rm should accept multiple arguments

Signed-off-by: Arash Deshmeh <adeshmeh@ca.ibm.com>
Arash Deshmeh 8 năm trước cách đây
mục cha
commit
ff0899ad2f

+ 153 - 0
cli/command/stack/client_test.go

@@ -0,0 +1,153 @@
+package stack
+
+import (
+	"strings"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/cli/compose/convert"
+	"github.com/docker/docker/client"
+	"golang.org/x/net/context"
+)
+
+type fakeClient struct {
+	client.Client
+
+	services []string
+	networks []string
+	secrets  []string
+
+	removedServices []string
+	removedNetworks []string
+	removedSecrets  []string
+
+	serviceListFunc   func(options types.ServiceListOptions) ([]swarm.Service, error)
+	networkListFunc   func(options types.NetworkListOptions) ([]types.NetworkResource, error)
+	secretListFunc    func(options types.SecretListOptions) ([]swarm.Secret, error)
+	serviceRemoveFunc func(serviceID string) error
+	networkRemoveFunc func(networkID string) error
+	secretRemoveFunc  func(secretID string) error
+}
+
+func (cli *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
+	if cli.serviceListFunc != nil {
+		return cli.serviceListFunc(options)
+	}
+
+	namespace := namespaceFromFilters(options.Filters)
+	servicesList := []swarm.Service{}
+	for _, name := range cli.services {
+		if belongToNamespace(name, namespace) {
+			servicesList = append(servicesList, serviceFromName(name))
+		}
+	}
+	return servicesList, nil
+}
+
+func (cli *fakeClient) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
+	if cli.networkListFunc != nil {
+		return cli.networkListFunc(options)
+	}
+
+	namespace := namespaceFromFilters(options.Filters)
+	networksList := []types.NetworkResource{}
+	for _, name := range cli.networks {
+		if belongToNamespace(name, namespace) {
+			networksList = append(networksList, networkFromName(name))
+		}
+	}
+	return networksList, nil
+}
+
+func (cli *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
+	if cli.secretListFunc != nil {
+		return cli.secretListFunc(options)
+	}
+
+	namespace := namespaceFromFilters(options.Filters)
+	secretsList := []swarm.Secret{}
+	for _, name := range cli.secrets {
+		if belongToNamespace(name, namespace) {
+			secretsList = append(secretsList, secretFromName(name))
+		}
+	}
+	return secretsList, nil
+}
+
+func (cli *fakeClient) ServiceRemove(ctx context.Context, serviceID string) error {
+	if cli.serviceRemoveFunc != nil {
+		return cli.serviceRemoveFunc(serviceID)
+	}
+
+	cli.removedServices = append(cli.removedServices, serviceID)
+	return nil
+}
+
+func (cli *fakeClient) NetworkRemove(ctx context.Context, networkID string) error {
+	if cli.networkRemoveFunc != nil {
+		return cli.networkRemoveFunc(networkID)
+	}
+
+	cli.removedNetworks = append(cli.removedNetworks, networkID)
+	return nil
+}
+
+func (cli *fakeClient) SecretRemove(ctx context.Context, secretID string) error {
+	if cli.secretRemoveFunc != nil {
+		return cli.secretRemoveFunc(secretID)
+	}
+
+	cli.removedSecrets = append(cli.removedSecrets, secretID)
+	return nil
+}
+
+func serviceFromName(name string) swarm.Service {
+	return swarm.Service{
+		ID: "ID-" + name,
+		Spec: swarm.ServiceSpec{
+			Annotations: swarm.Annotations{Name: name},
+		},
+	}
+}
+
+func networkFromName(name string) types.NetworkResource {
+	return types.NetworkResource{
+		ID:   "ID-" + name,
+		Name: name,
+	}
+}
+
+func secretFromName(name string) swarm.Secret {
+	return swarm.Secret{
+		ID: "ID-" + name,
+		Spec: swarm.SecretSpec{
+			Annotations: swarm.Annotations{Name: name},
+		},
+	}
+}
+
+func namespaceFromFilters(filters filters.Args) string {
+	label := filters.Get("label")[0]
+	return strings.TrimPrefix(label, convert.LabelNamespace+"=")
+}
+
+func belongToNamespace(id, namespace string) bool {
+	return strings.HasPrefix(id, namespace+"_")
+}
+
+func objectName(namespace, name string) string {
+	return namespace + "_" + name
+}
+
+func objectID(name string) string {
+	return "ID-" + name
+}
+
+func buildObjectIDs(objectNames []string) []string {
+	IDs := make([]string, len(objectNames))
+	for i, name := range objectNames {
+		IDs[i] = objectID(name)
+	}
+	return IDs
+}

+ 2 - 29
cli/command/stack/deploy_test.go

@@ -4,39 +4,12 @@ import (
 	"bytes"
 	"testing"
 
-	"github.com/docker/docker/api/types"
-	"github.com/docker/docker/api/types/swarm"
 	"github.com/docker/docker/cli/compose/convert"
 	"github.com/docker/docker/cli/internal/test"
-	"github.com/docker/docker/client"
 	"github.com/docker/docker/pkg/testutil/assert"
 	"golang.org/x/net/context"
 )
 
-type fakeClient struct {
-	client.Client
-	serviceList []string
-	removedIDs  []string
-}
-
-func (cli *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
-	services := []swarm.Service{}
-	for _, name := range cli.serviceList {
-		services = append(services, swarm.Service{
-			ID: name,
-			Spec: swarm.ServiceSpec{
-				Annotations: swarm.Annotations{Name: name},
-			},
-		})
-	}
-	return services, nil
-}
-
-func (cli *fakeClient) ServiceRemove(ctx context.Context, serviceID string) error {
-	cli.removedIDs = append(cli.removedIDs, serviceID)
-	return nil
-}
-
 func TestPruneServices(t *testing.T) {
 	ctx := context.Background()
 	namespace := convert.NewNamespace("foo")
@@ -44,11 +17,11 @@ func TestPruneServices(t *testing.T) {
 		"new":  {},
 		"keep": {},
 	}
-	client := &fakeClient{serviceList: []string{"foo_keep", "foo_remove"}}
+	client := &fakeClient{services: []string{objectName("foo", "keep"), objectName("foo", "remove")}}
 	dockerCli := test.NewFakeCli(client, &bytes.Buffer{})
 	dockerCli.SetErr(&bytes.Buffer{})
 
 	pruneServices(ctx, dockerCli, namespace, services)
 
-	assert.DeepEqual(t, client.removedIDs, []string{"foo_remove"})
+	assert.DeepEqual(t, client.removedServices, buildObjectIDs([]string{objectName("foo", "remove")}))
 }

+ 37 - 29
cli/command/stack/remove.go

@@ -2,6 +2,7 @@ package stack
 
 import (
 	"fmt"
+	"strings"
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/swarm"
@@ -13,56 +14,63 @@ import (
 )
 
 type removeOptions struct {
-	namespace string
+	namespaces []string
 }
 
-func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
+func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
 	var opts removeOptions
 
 	cmd := &cobra.Command{
-		Use:     "rm STACK",
+		Use:     "rm STACK [STACK...]",
 		Aliases: []string{"remove", "down"},
-		Short:   "Remove the stack",
-		Args:    cli.ExactArgs(1),
+		Short:   "Remove one or more stacks",
+		Args:    cli.RequiresMinArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
-			opts.namespace = args[0]
+			opts.namespaces = args
 			return runRemove(dockerCli, opts)
 		},
 	}
 	return cmd
 }
 
-func runRemove(dockerCli *command.DockerCli, opts removeOptions) error {
-	namespace := opts.namespace
+func runRemove(dockerCli command.Cli, opts removeOptions) error {
+	namespaces := opts.namespaces
 	client := dockerCli.Client()
 	ctx := context.Background()
 
-	services, err := getServices(ctx, client, namespace)
-	if err != nil {
-		return err
-	}
+	var errs []string
+	for _, namespace := range namespaces {
+		services, err := getServices(ctx, client, namespace)
+		if err != nil {
+			return err
+		}
 
-	networks, err := getStackNetworks(ctx, client, namespace)
-	if err != nil {
-		return err
-	}
+		networks, err := getStackNetworks(ctx, client, namespace)
+		if err != nil {
+			return err
+		}
 
-	secrets, err := getStackSecrets(ctx, client, namespace)
-	if err != nil {
-		return err
-	}
+		secrets, err := getStackSecrets(ctx, client, namespace)
+		if err != nil {
+			return err
+		}
 
-	if len(services)+len(networks)+len(secrets) == 0 {
-		fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace)
-		return nil
-	}
+		if len(services)+len(networks)+len(secrets) == 0 {
+			fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace)
+			continue
+		}
+
+		hasError := removeServices(ctx, dockerCli, services)
+		hasError = removeSecrets(ctx, dockerCli, secrets) || hasError
+		hasError = removeNetworks(ctx, dockerCli, networks) || hasError
 
-	hasError := removeServices(ctx, dockerCli, services)
-	hasError = removeSecrets(ctx, dockerCli, secrets) || hasError
-	hasError = removeNetworks(ctx, dockerCli, networks) || hasError
+		if hasError {
+			errs = append(errs, fmt.Sprintf("Failed to remove some resources from stack: %s", namespace))
+		}
+	}
 
-	if hasError {
-		return errors.Errorf("Failed to remove some resources")
+	if len(errs) > 0 {
+		return errors.Errorf(strings.Join(errs, "\n"))
 	}
 	return nil
 }

+ 107 - 0
cli/command/stack/remove_test.go

@@ -0,0 +1,107 @@
+package stack
+
+import (
+	"bytes"
+	"errors"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/cli/internal/test"
+	"github.com/docker/docker/pkg/testutil/assert"
+)
+
+func TestRemoveStack(t *testing.T) {
+	allServices := []string{
+		objectName("foo", "service1"),
+		objectName("foo", "service2"),
+		objectName("bar", "service1"),
+		objectName("bar", "service2"),
+	}
+	allServicesIDs := buildObjectIDs(allServices)
+
+	allNetworks := []string{
+		objectName("foo", "network1"),
+		objectName("bar", "network1"),
+	}
+	allNetworksIDs := buildObjectIDs(allNetworks)
+
+	allSecrets := []string{
+		objectName("foo", "secret1"),
+		objectName("foo", "secret2"),
+		objectName("bar", "secret1"),
+	}
+	allSecretsIDs := buildObjectIDs(allSecrets)
+
+	cli := &fakeClient{
+		services: allServices,
+		networks: allNetworks,
+		secrets:  allSecrets,
+	}
+	cmd := newRemoveCommand(test.NewFakeCli(cli, &bytes.Buffer{}))
+	cmd.SetArgs([]string{"foo", "bar"})
+
+	assert.NilError(t, cmd.Execute())
+	assert.DeepEqual(t, cli.removedServices, allServicesIDs)
+	assert.DeepEqual(t, cli.removedNetworks, allNetworksIDs)
+	assert.DeepEqual(t, cli.removedSecrets, allSecretsIDs)
+}
+
+func TestSkipEmptyStack(t *testing.T) {
+	buf := new(bytes.Buffer)
+	allServices := []string{objectName("bar", "service1"), objectName("bar", "service2")}
+	allServicesIDs := buildObjectIDs(allServices)
+
+	allNetworks := []string{objectName("bar", "network1")}
+	allNetworksIDs := buildObjectIDs(allNetworks)
+
+	allSecrets := []string{objectName("bar", "secret1")}
+	allSecretsIDs := buildObjectIDs(allSecrets)
+
+	cli := &fakeClient{
+		services: allServices,
+		networks: allNetworks,
+		secrets:  allSecrets,
+	}
+	cmd := newRemoveCommand(test.NewFakeCli(cli, buf))
+	cmd.SetArgs([]string{"foo", "bar"})
+
+	assert.NilError(t, cmd.Execute())
+	assert.Contains(t, buf.String(), "Nothing found in stack: foo")
+	assert.DeepEqual(t, cli.removedServices, allServicesIDs)
+	assert.DeepEqual(t, cli.removedNetworks, allNetworksIDs)
+	assert.DeepEqual(t, cli.removedSecrets, allSecretsIDs)
+}
+
+func TestContinueAfterError(t *testing.T) {
+	allServices := []string{objectName("foo", "service1"), objectName("bar", "service1")}
+	allServicesIDs := buildObjectIDs(allServices)
+
+	allNetworks := []string{objectName("foo", "network1"), objectName("bar", "network1")}
+	allNetworksIDs := buildObjectIDs(allNetworks)
+
+	allSecrets := []string{objectName("foo", "secret1"), objectName("bar", "secret1")}
+	allSecretsIDs := buildObjectIDs(allSecrets)
+
+	removedServices := []string{}
+	cli := &fakeClient{
+		services: allServices,
+		networks: allNetworks,
+		secrets:  allSecrets,
+
+		serviceRemoveFunc: func(serviceID string) error {
+			removedServices = append(removedServices, serviceID)
+
+			if strings.Contains(serviceID, "foo") {
+				return errors.New("")
+			}
+			return nil
+		},
+	}
+	cmd := newRemoveCommand(test.NewFakeCli(cli, &bytes.Buffer{}))
+	cmd.SetArgs([]string{"foo", "bar"})
+
+	assert.Error(t, cmd.Execute(), "Failed to remove some resources from stack: foo")
+	assert.DeepEqual(t, removedServices, allServicesIDs)
+	assert.DeepEqual(t, cli.removedNetworks, allNetworksIDs)
+	assert.DeepEqual(t, cli.removedSecrets, allSecretsIDs)
+}

+ 40 - 2
docs/reference/commandline/stack_rm.md

@@ -16,9 +16,9 @@ keywords: "stack, rm, remove, down"
 # stack rm
 
 ```markdown
-Usage:  docker stack rm STACK
+Usage:  docker stack rm STACK [STACK...]
 
-Remove the stack
+Remove one or more stacks
 
 Aliases:
   rm, remove, down
@@ -32,6 +32,44 @@ Options:
 Remove the stack from the swarm. This command has to be run targeting
 a manager node.
 
+## Examples
+
+### Remove a stack
+
+This will remove the stack with the name `myapp`. Services, networks, and secrets associated with the stack will be removed.
+
+```bash
+$ docker stack rm myapp
+
+Removing service myapp_redis
+Removing service myapp_web
+Removing service myapp_lb
+Removing network myapp_default
+Removing network myapp_frontend
+```
+
+### Remove multiple stacks
+
+This will remove all the specified stacks, `myapp` and `vossibility`. Services, networks, and secrets associated with all the specified stacks will be removed.
+
+```bash
+$ docker stack rm myapp vossibility
+
+Removing service myapp_redis
+Removing service myapp_web
+Removing service myapp_lb
+Removing network myapp_default
+Removing network myapp_frontend
+Removing service vossibility_nsqd
+Removing service vossibility_logstash
+Removing service vossibility_elasticsearch
+Removing service vossibility_kibana
+Removing service vossibility_ghollector
+Removing service vossibility_lookupd
+Removing network vossibility_default
+Removing network vossibility_vossibility
+```
+
 ## Related commands
 
 * [stack deploy](stack_deploy.md)