diff --git a/cli/command/stack/options/opts.go b/cli/command/stack/options/opts.go index 263b493b6a..28d4c32622 100644 --- a/cli/command/stack/options/opts.go +++ b/cli/command/stack/options/opts.go @@ -38,6 +38,7 @@ type PS struct { // Remove holds docker stack remove options type Remove struct { Namespaces []string + Detach bool } // Services holds docker stack services options diff --git a/cli/command/stack/remove.go b/cli/command/stack/remove.go index 0018624bce..18ad5a25bf 100644 --- a/cli/command/stack/remove.go +++ b/cli/command/stack/remove.go @@ -27,5 +27,8 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command { return completeNames(dockerCli)(cmd, args, toComplete) }, } + + flags := cmd.Flags() + flags.BoolVarP(&opts.Detach, "detach", "d", true, "Do not wait for stack removal") return cmd } diff --git a/cli/command/stack/swarm/common.go b/cli/command/stack/swarm/common.go index 12731c0d88..a7485f2e18 100644 --- a/cli/command/stack/swarm/common.go +++ b/cli/command/stack/swarm/common.go @@ -44,3 +44,7 @@ func getStackSecrets(ctx context.Context, apiclient client.APIClient, namespace func getStackConfigs(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Config, error) { return apiclient.ConfigList(ctx, types.ConfigListOptions{Filters: getStackFilter(namespace)}) } + +func getStackTasks(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Task, error) { + return apiclient.TaskList(ctx, types.TaskListOptions{Filters: getStackFilter(namespace)}) +} diff --git a/cli/command/stack/swarm/remove.go b/cli/command/stack/swarm/remove.go index 6dc49a1ce7..5a901fbf8c 100644 --- a/cli/command/stack/swarm/remove.go +++ b/cli/command/stack/swarm/remove.go @@ -11,6 +11,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/versions" + apiclient "github.com/docker/docker/client" "github.com/pkg/errors" ) @@ -58,6 +59,14 @@ func RunRemove(ctx context.Context, dockerCli command.Cli, opts options.Remove) if hasError { errs = append(errs, fmt.Sprintf("Failed to remove some resources from stack: %s", namespace)) + continue + } + + if !opts.Detach { + err = waitOnTasks(ctx, client, namespace) + if err != nil { + errs = append(errs, fmt.Sprintf("Failed to wait on tasks of stack: %s: %s", namespace, err)) + } } } @@ -137,3 +146,45 @@ func removeConfigs( } return hasError } + +var numberedStates = map[swarm.TaskState]int64{ + swarm.TaskStateNew: 1, + swarm.TaskStateAllocated: 2, + swarm.TaskStatePending: 3, + swarm.TaskStateAssigned: 4, + swarm.TaskStateAccepted: 5, + swarm.TaskStatePreparing: 6, + swarm.TaskStateReady: 7, + swarm.TaskStateStarting: 8, + swarm.TaskStateRunning: 9, + swarm.TaskStateComplete: 10, + swarm.TaskStateShutdown: 11, + swarm.TaskStateFailed: 12, + swarm.TaskStateRejected: 13, +} + +func terminalState(state swarm.TaskState) bool { + return numberedStates[state] > numberedStates[swarm.TaskStateRunning] +} + +func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace string) error { + terminalStatesReached := 0 + for { + tasks, err := getStackTasks(ctx, client, namespace) + if err != nil { + return fmt.Errorf("failed to get tasks: %w", err) + } + + for _, task := range tasks { + if terminalState(task.Status.State) { + terminalStatesReached++ + break + } + } + + if terminalStatesReached == len(tasks) { + break + } + } + return nil +} diff --git a/docs/reference/commandline/stack_rm.md b/docs/reference/commandline/stack_rm.md index 9549ea28f6..5cfbfaab1b 100644 --- a/docs/reference/commandline/stack_rm.md +++ b/docs/reference/commandline/stack_rm.md @@ -7,6 +7,12 @@ Remove one or more stacks `docker stack rm`, `docker stack remove`, `docker stack down` +### Options + +| Name | Type | Default | Description | +|:-----------------|:-------|:--------|:------------------------------| +| `-d`, `--detach` | `bool` | `true` | Do not wait for stack removal | +