From ff0899ad2f9f94f541b45047798dbd469d72038a Mon Sep 17 00:00:00 2001 From: Arash Deshmeh Date: Sun, 26 Mar 2017 02:23:24 -0400 Subject: [PATCH] stack rm should accept multiple arguments Signed-off-by: Arash Deshmeh --- cli/command/stack/client_test.go | 153 +++++++++++++++++++++++++ cli/command/stack/deploy_test.go | 31 +---- cli/command/stack/remove.go | 72 ++++++------ cli/command/stack/remove_test.go | 107 +++++++++++++++++ docs/reference/commandline/stack_rm.md | 42 ++++++- 5 files changed, 342 insertions(+), 63 deletions(-) create mode 100644 cli/command/stack/client_test.go create mode 100644 cli/command/stack/remove_test.go diff --git a/cli/command/stack/client_test.go b/cli/command/stack/client_test.go new file mode 100644 index 0000000000..0cd8612b6d --- /dev/null +++ b/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 +} diff --git a/cli/command/stack/deploy_test.go b/cli/command/stack/deploy_test.go index dac1350547..328222af53 100644 --- a/cli/command/stack/deploy_test.go +++ b/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")})) } diff --git a/cli/command/stack/remove.go b/cli/command/stack/remove.go index e976eccdaa..7df4e4c0ed 100644 --- a/cli/command/stack/remove.go +++ b/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 + } + + 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) + continue + } + + 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)) + } } - networks, err := getStackNetworks(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 - } - - hasError := removeServices(ctx, dockerCli, services) - hasError = removeSecrets(ctx, dockerCli, secrets) || hasError - hasError = removeNetworks(ctx, dockerCli, networks) || hasError - - if hasError { - return errors.Errorf("Failed to remove some resources") + if len(errs) > 0 { + return errors.Errorf(strings.Join(errs, "\n")) } return nil } diff --git a/cli/command/stack/remove_test.go b/cli/command/stack/remove_test.go new file mode 100644 index 0000000000..7f64fb5505 --- /dev/null +++ b/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) +} diff --git a/docs/reference/commandline/stack_rm.md b/docs/reference/commandline/stack_rm.md index d9d50433a7..a1854ae6f0 100644 --- a/docs/reference/commandline/stack_rm.md +++ b/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)