diff --git a/cli/command/stack/remove.go b/cli/command/stack/remove.go index d95171aabf..157f71ea12 100644 --- a/cli/command/stack/remove.go +++ b/cli/command/stack/remove.go @@ -2,6 +2,7 @@ package stack import ( "fmt" + "sort" "strings" "github.com/docker/cli/cli" @@ -88,12 +89,19 @@ func runRemove(dockerCli command.Cli, opts removeOptions) error { return nil } +func sortServiceByName(services []swarm.Service) func(i, j int) bool { + return func(i, j int) bool { + return services[i].Spec.Name < services[j].Spec.Name + } +} + func removeServices( ctx context.Context, dockerCli command.Cli, services []swarm.Service, ) bool { var hasError bool + sort.Slice(services, sortServiceByName(services)) for _, service := range services { fmt.Fprintf(dockerCli.Err(), "Removing service %s\n", service.Spec.Name) if err := dockerCli.Client().ServiceRemove(ctx, service.ID); err != nil { diff --git a/e2e/stack/main_test.go b/e2e/stack/main_test.go new file mode 100644 index 0000000000..74081f457b --- /dev/null +++ b/e2e/stack/main_test.go @@ -0,0 +1,26 @@ +package stack + +import ( + "fmt" + "os" + "testing" + + "github.com/pkg/errors" +) + +func TestMain(m *testing.M) { + if err := setupTestEnv(); err != nil { + fmt.Println(err.Error()) + os.Exit(3) + } + os.Exit(m.Run()) +} + +// TODO: move to shared internal package +func setupTestEnv() error { + dockerHost := os.Getenv("TEST_DOCKER_HOST") + if dockerHost == "" { + return errors.New("$TEST_DOCKER_HOST must be set") + } + return os.Setenv("DOCKER_HOST", dockerHost) +} diff --git a/e2e/stack/remove_test.go b/e2e/stack/remove_test.go new file mode 100644 index 0000000000..77d95c793d --- /dev/null +++ b/e2e/stack/remove_test.go @@ -0,0 +1,89 @@ +package stack + +import ( + "fmt" + "strings" + "testing" + "time" + + shlex "github.com/flynn-archive/go-shlex" + "github.com/gotestyourself/gotestyourself/golden" + "github.com/gotestyourself/gotestyourself/icmd" + "github.com/stretchr/testify/require" +) + +func TestRemove(t *testing.T) { + stackname := "test-stack-remove" + deployFullStack(t, stackname) + defer cleanupFullStack(t, stackname) + + result := icmd.RunCmd(shell(t, "docker stack rm %s", stackname)) + + result.Assert(t, icmd.Expected{Out: icmd.None}) + golden.Assert(t, result.Stderr(), "stack-remove-success.golden") +} + +func deployFullStack(t *testing.T, stackname string) { + // TODO: this stack should have full options not minimal options + result := icmd.RunCmd(shell(t, + "docker stack deploy --compose-file=./testdata/full-stack.yml %s", stackname)) + result.Assert(t, icmd.Success) + + waitOn(t, taskCount(stackname, 2), 0) +} + +func cleanupFullStack(t *testing.T, stackname string) { + result := icmd.RunCmd(shell(t, "docker stack rm %s", stackname)) + result.Assert(t, icmd.Success) + waitOn(t, taskCount(stackname, 0), 0) +} + +func taskCount(stackname string, expected int) func() (bool, error) { + return func() (bool, error) { + result := icmd.RunCommand( + "docker", "stack", "ps", "-f=desired-state=running", stackname) + count := lines(result.Stdout()) - 1 + return count == expected, nil + } +} + +func lines(out string) int { + return len(strings.Split(strings.TrimSpace(out), "\n")) +} + +// TODO: move to gotestyourself +func shell(t *testing.T, format string, args ...interface{}) icmd.Cmd { + cmd, err := shlex.Split(fmt.Sprintf(format, args...)) + require.NoError(t, err) + return icmd.Cmd{Command: cmd} +} + +// TODO: move to gotestyourself +func waitOn(t *testing.T, check func() (bool, error), timeout time.Duration) { + if timeout == time.Duration(0) { + timeout = defaultTimeout() + } + + after := time.After(timeout) + for { + select { + case <-after: + // TODO: include check function name in error message + t.Fatalf("timeout hit after %s", timeout) + default: + // TODO: maybe return a failure message as well? + done, err := check() + if done { + return + } + if err != nil { + t.Fatal(err.Error()) + } + } + } +} + +func defaultTimeout() time.Duration { + // TODO: support override from environment variable + return 10 * time.Second +} diff --git a/e2e/stack/testdata/full-stack.yml b/e2e/stack/testdata/full-stack.yml new file mode 100644 index 0000000000..8c4d06f854 --- /dev/null +++ b/e2e/stack/testdata/full-stack.yml @@ -0,0 +1,9 @@ +version: '3.3' + +services: + one: + image: registry:5000/alpine:3.6 + command: top + two: + image: registry:5000/alpine:3.6 + command: top diff --git a/e2e/stack/testdata/stack-remove-success.golden b/e2e/stack/testdata/stack-remove-success.golden new file mode 100644 index 0000000000..f41a891702 --- /dev/null +++ b/e2e/stack/testdata/stack-remove-success.golden @@ -0,0 +1,3 @@ +Removing service test-stack-remove_one +Removing service test-stack-remove_two +Removing network test-stack-remove_default