From 8e0f32c269f5722d3ac763e7de8edfb3b655e4c1 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 23 Jun 2017 09:56:31 +0200 Subject: [PATCH 1/4] =?UTF-8?q?Add=20a=20line=20break=20after=20warning=20?= =?UTF-8?q?=F0=9F=91=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Vincent Demeester (cherry picked from commit b7fad8f1dc07005b03876bdb19460f419f1a1690) Signed-off-by: Sebastiaan van Stijn --- components/cli/cli/command/stack/remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cli/command/stack/remove.go b/components/cli/cli/command/stack/remove.go index b63c385a7e..20d3ee71db 100644 --- a/components/cli/cli/command/stack/remove.go +++ b/components/cli/cli/command/stack/remove.go @@ -63,7 +63,7 @@ func runRemove(dockerCli command.Cli, opts removeOptions) error { return err } if versions.LessThan(version.APIVersion, "1.30") { - fmt.Fprintf(dockerCli.Err(), `WARNING: ignoring "configs" (requires API version 1.30, but the Docker daemon API version is %s)`, version.APIVersion) + fmt.Fprintf(dockerCli.Err(), "WARNING: ignoring \"configs\" (requires API version 1.30, but the Docker daemon API version is %s)\n", version.APIVersion) } else { configs, err = getStackConfigs(ctx, client, namespace) if err != nil { From 6082f71f1594e0919465f8b24703000a44d57e81 Mon Sep 17 00:00:00 2001 From: Arash Deshmeh Date: Tue, 20 Jun 2017 14:00:01 -0400 Subject: [PATCH 2/4] add unit tests to stack package Signed-off-by: Arash Deshmeh (cherry picked from commit 535af2d8689ae33e78ac45de9b70553845efea48) Signed-off-by: Sebastiaan van Stijn --- components/cli/cli/command/node/ps_test.go | 6 +- .../cli/cli/command/stack/client_test.go | 40 +++- components/cli/cli/command/stack/list.go | 4 +- components/cli/cli/command/stack/list_test.go | 122 ++++++++++++ components/cli/cli/command/stack/opts_test.go | 49 +++++ components/cli/cli/command/stack/ps_test.go | 185 ++++++++++++++++++ .../cli/cli/command/stack/remove_test.go | 2 +- components/cli/cli/command/stack/services.go | 4 +- .../cli/cli/command/stack/services_test.go | 180 +++++++++++++++++ .../testdata/bundlefile_with_two_services.dab | 29 +++ .../stack/testdata/stack-list-sort.golden | 3 + .../testdata/stack-list-with-format.golden | 1 + .../testdata/stack-list-without-format.golden | 2 + .../stack-ps-with-config-format.golden | 1 + .../testdata/stack-ps-with-format.golden | 1 + .../stack-ps-with-no-resolve-option.golden | 1 + .../stack-ps-with-no-trunc-option.golden | 1 + .../stack-ps-with-quiet-option.golden | 1 + .../testdata/stack-ps-without-format.golden | 2 + .../stack-services-with-config-format.golden | 1 + .../stack-services-with-format.golden | 1 + .../stack-services-with-quiet-option.golden | 1 + .../stack-services-without-format.golden | 2 + .../cli/cli/internal/test/builders/service.go | 36 ++++ .../cli/cli/internal/test/builders/task.go | 46 ++++- 25 files changed, 703 insertions(+), 18 deletions(-) create mode 100644 components/cli/cli/command/stack/list_test.go create mode 100644 components/cli/cli/command/stack/opts_test.go create mode 100644 components/cli/cli/command/stack/ps_test.go create mode 100644 components/cli/cli/command/stack/services_test.go create mode 100644 components/cli/cli/command/stack/testdata/bundlefile_with_two_services.dab create mode 100644 components/cli/cli/command/stack/testdata/stack-list-sort.golden create mode 100644 components/cli/cli/command/stack/testdata/stack-list-with-format.golden create mode 100644 components/cli/cli/command/stack/testdata/stack-list-without-format.golden create mode 100644 components/cli/cli/command/stack/testdata/stack-ps-with-config-format.golden create mode 100644 components/cli/cli/command/stack/testdata/stack-ps-with-format.golden create mode 100644 components/cli/cli/command/stack/testdata/stack-ps-with-no-resolve-option.golden create mode 100644 components/cli/cli/command/stack/testdata/stack-ps-with-no-trunc-option.golden create mode 100644 components/cli/cli/command/stack/testdata/stack-ps-with-quiet-option.golden create mode 100644 components/cli/cli/command/stack/testdata/stack-ps-without-format.golden create mode 100644 components/cli/cli/command/stack/testdata/stack-services-with-config-format.golden create mode 100644 components/cli/cli/command/stack/testdata/stack-services-with-format.golden create mode 100644 components/cli/cli/command/stack/testdata/stack-services-with-quiet-option.golden create mode 100644 components/cli/cli/command/stack/testdata/stack-services-without-format.golden diff --git a/components/cli/cli/command/node/ps_test.go b/components/cli/cli/command/node/ps_test.go index ddc0eba0d4..d25a55f0cf 100644 --- a/components/cli/cli/command/node/ps_test.go +++ b/components/cli/cli/command/node/ps_test.go @@ -103,11 +103,11 @@ func TestNodePs(t *testing.T) { }, taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { return []swarm.Task{ - *Task(TaskID("taskID1"), ServiceID("failure"), + *Task(TaskID("taskID1"), TaskServiceID("failure"), WithStatus(Timestamp(time.Now().Add(-2*time.Hour)), StatusErr("a task error"))), - *Task(TaskID("taskID2"), ServiceID("failure"), + *Task(TaskID("taskID2"), TaskServiceID("failure"), WithStatus(Timestamp(time.Now().Add(-3*time.Hour)), StatusErr("a task error"))), - *Task(TaskID("taskID3"), ServiceID("failure"), + *Task(TaskID("taskID3"), TaskServiceID("failure"), WithStatus(Timestamp(time.Now().Add(-4*time.Hour)), StatusErr("a task error"))), }, nil }, diff --git a/components/cli/cli/command/stack/client_test.go b/components/cli/cli/command/stack/client_test.go index 22be9dc06a..50442783fa 100644 --- a/components/cli/cli/command/stack/client_test.go +++ b/components/cli/cli/command/stack/client_test.go @@ -25,14 +25,17 @@ type fakeClient struct { removedSecrets []string removedConfigs []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) - configListFunc func(options types.ConfigListOptions) ([]swarm.Config, error) - serviceRemoveFunc func(serviceID string) error - networkRemoveFunc func(networkID string) error - secretRemoveFunc func(secretID string) error - configRemoveFunc func(configID string) error + serviceListFunc func(options types.ServiceListOptions) ([]swarm.Service, error) + networkListFunc func(options types.NetworkListOptions) ([]types.NetworkResource, error) + secretListFunc func(options types.SecretListOptions) ([]swarm.Secret, error) + configListFunc func(options types.ConfigListOptions) ([]swarm.Config, error) + nodeListFunc func(options types.NodeListOptions) ([]swarm.Node, error) + taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error) + nodeInspectWithRaw func(ref string) (swarm.Node, []byte, error) + serviceRemoveFunc func(serviceID string) error + networkRemoveFunc func(networkID string) error + secretRemoveFunc func(secretID string) error + configRemoveFunc func(configID string) error } func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) { @@ -102,6 +105,27 @@ func (cli *fakeClient) ConfigList(ctx context.Context, options types.ConfigListO return configsList, nil } +func (cli *fakeClient) TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) { + if cli.taskListFunc != nil { + return cli.taskListFunc(options) + } + return []swarm.Task{}, nil +} + +func (cli *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) { + if cli.nodeListFunc != nil { + return cli.nodeListFunc(options) + } + return []swarm.Node{}, nil +} + +func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, ref string) (swarm.Node, []byte, error) { + if cli.nodeInspectWithRaw != nil { + return cli.nodeInspectWithRaw(ref) + } + return swarm.Node{}, nil, nil +} + func (cli *fakeClient) ServiceRemove(ctx context.Context, serviceID string) error { if cli.serviceRemoveFunc != nil { return cli.serviceRemoveFunc(serviceID) diff --git a/components/cli/cli/command/stack/list.go b/components/cli/cli/command/stack/list.go index 4560a98ec9..a14af0ad21 100644 --- a/components/cli/cli/command/stack/list.go +++ b/components/cli/cli/command/stack/list.go @@ -18,7 +18,7 @@ type listOptions struct { format string } -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { +func newListCommand(dockerCli command.Cli) *cobra.Command { opts := listOptions{} cmd := &cobra.Command{ @@ -36,7 +36,7 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runList(dockerCli *command.DockerCli, opts listOptions) error { +func runList(dockerCli command.Cli, opts listOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/components/cli/cli/command/stack/list_test.go b/components/cli/cli/command/stack/list_test.go new file mode 100644 index 0000000000..4f258977ed --- /dev/null +++ b/components/cli/cli/command/stack/list_test.go @@ -0,0 +1,122 @@ +package stack + +import ( + "bytes" + "io/ioutil" + "testing" + + "github.com/docker/cli/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/cli/cli/internal/test/builders" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/pkg/testutil" + "github.com/docker/docker/pkg/testutil/golden" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestListErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + serviceListFunc func(options types.ServiceListOptions) ([]swarm.Service, error) + expectedError string + }{ + { + args: []string{"foo"}, + expectedError: "accepts no argument", + }, + { + flags: map[string]string{ + "format": "{{invalid format}}", + }, + expectedError: "Template parsing error", + }, + { + serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { + return []swarm.Service{}, errors.Errorf("error getting services") + }, + expectedError: "error getting services", + }, + { + serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { + return []swarm.Service{*Service()}, nil + }, + expectedError: "cannot get label", + }, + } + + for _, tc := range testCases { + cmd := newListCommand(test.NewFakeCli(&fakeClient{ + serviceListFunc: tc.serviceListFunc, + }, &bytes.Buffer{})) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) + } +} + +func TestListWithFormat(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newListCommand(test.NewFakeCli(&fakeClient{ + serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { + return []swarm.Service{ + *Service( + ServiceLabels(map[string]string{ + "com.docker.stack.namespace": "service-name-foo", + }), + )}, nil + }, + }, buf)) + cmd.Flags().Set("format", "{{ .Name }}") + assert.NoError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "stack-list-with-format.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} + +func TestListWithoutFormat(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newListCommand(test.NewFakeCli(&fakeClient{ + serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { + return []swarm.Service{ + *Service( + ServiceLabels(map[string]string{ + "com.docker.stack.namespace": "service-name-foo", + }), + )}, nil + }, + }, buf)) + assert.NoError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "stack-list-without-format.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} + +func TestListOrder(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newListCommand(test.NewFakeCli(&fakeClient{ + serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { + return []swarm.Service{ + *Service( + ServiceLabels(map[string]string{ + "com.docker.stack.namespace": "service-name-foo", + }), + ), + *Service( + ServiceLabels(map[string]string{ + "com.docker.stack.namespace": "service-name-bar", + }), + ), + }, nil + }, + }, buf)) + assert.NoError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "stack-list-sort.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} diff --git a/components/cli/cli/command/stack/opts_test.go b/components/cli/cli/command/stack/opts_test.go new file mode 100644 index 0000000000..b57dcd89f6 --- /dev/null +++ b/components/cli/cli/command/stack/opts_test.go @@ -0,0 +1,49 @@ +package stack + +import ( + "bytes" + "fmt" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoadBundlefileErrors(t *testing.T) { + testCases := []struct { + namespace string + path string + expectedError error + }{ + { + namespace: "namespace_foo", + expectedError: fmt.Errorf("Bundle %s.dab not found", "namespace_foo"), + }, + { + namespace: "namespace_foo", + path: "invalid_path", + expectedError: fmt.Errorf("Bundle %s not found", "invalid_path"), + }, + { + namespace: "namespace_foo", + path: filepath.Join("testdata", "bundlefile_with_invalid_syntax"), + expectedError: fmt.Errorf("Error reading"), + }, + } + + for _, tc := range testCases { + _, err := loadBundlefile(&bytes.Buffer{}, tc.namespace, tc.path) + assert.Error(t, err, tc.expectedError) + } +} + +func TestLoadBundlefile(t *testing.T) { + buf := new(bytes.Buffer) + + namespace := "" + path := filepath.Join("testdata", "bundlefile_with_two_services.dab") + bundleFile, err := loadBundlefile(buf, namespace, path) + + assert.NoError(t, err) + assert.Equal(t, len(bundleFile.Services), 2) +} diff --git a/components/cli/cli/command/stack/ps_test.go b/components/cli/cli/command/stack/ps_test.go new file mode 100644 index 0000000000..afda419eb4 --- /dev/null +++ b/components/cli/cli/command/stack/ps_test.go @@ -0,0 +1,185 @@ +package stack + +import ( + "bytes" + "io/ioutil" + "testing" + "time" + + "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/cli/cli/internal/test/builders" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/pkg/testutil" + "github.com/docker/docker/pkg/testutil/golden" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestStackPsErrors(t *testing.T) { + testCases := []struct { + args []string + taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error) + expectedError string + }{ + + { + args: []string{}, + expectedError: "requires exactly 1 argument", + }, + { + args: []string{"foo", "bar"}, + expectedError: "requires exactly 1 argument", + }, + { + args: []string{"foo"}, + taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { + return nil, errors.Errorf("error getting tasks") + }, + expectedError: "error getting tasks", + }, + } + + for _, tc := range testCases { + cmd := newPsCommand(test.NewFakeCli(&fakeClient{ + taskListFunc: tc.taskListFunc, + }, &bytes.Buffer{})) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) + } +} + +func TestStackPsEmptyStack(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newPsCommand(test.NewFakeCli(&fakeClient{ + taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { + return []swarm.Task{}, nil + }, + }, buf)) + cmd.SetArgs([]string{"foo"}) + assert.NoError(t, cmd.Execute()) + testutil.EqualNormalizedString(t, testutil.RemoveSpace, buf.String(), "Nothing found in stack: foo") +} + +func TestStackPsWithQuietOption(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { + return []swarm.Task{*Task(TaskID("id-foo"))}, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newPsCommand(cli) + cmd.SetArgs([]string{"foo"}) + cmd.Flags().Set("quiet", "true") + assert.NoError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "stack-ps-with-quiet-option.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) + +} + +func TestStackPsWithNoTruncOption(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { + return []swarm.Task{*Task(TaskID("xn4cypcov06f2w8gsbaf2lst3"))}, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newPsCommand(cli) + cmd.SetArgs([]string{"foo"}) + cmd.Flags().Set("no-trunc", "true") + cmd.Flags().Set("format", "{{ .ID }}") + assert.NoError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "stack-ps-with-no-trunc-option.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} + +func TestStackPsWithNoResolveOption(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { + return []swarm.Task{*Task( + TaskNodeID("id-node-foo"), + )}, nil + }, + nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) { + return *Node(NodeName("node-name-bar")), nil, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newPsCommand(cli) + cmd.SetArgs([]string{"foo"}) + cmd.Flags().Set("no-resolve", "true") + cmd.Flags().Set("format", "{{ .Node }}") + assert.NoError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "stack-ps-with-no-resolve-option.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} + +func TestStackPsWithFormat(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { + return []swarm.Task{*Task(TaskServiceID("service-id-foo"))}, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newPsCommand(cli) + cmd.SetArgs([]string{"foo"}) + cmd.Flags().Set("format", "{{ .Name }}") + assert.NoError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "stack-ps-with-format.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} + +func TestStackPsWithConfigFormat(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { + return []swarm.Task{*Task(TaskServiceID("service-id-foo"))}, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{ + TasksFormat: "{{ .Name }}", + }) + cmd := newPsCommand(cli) + cmd.SetArgs([]string{"foo"}) + assert.NoError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "stack-ps-with-config-format.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} + +func TestStackPsWithoutFormat(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { + return []swarm.Task{*Task( + TaskID("id-foo"), + TaskServiceID("service-id-foo"), + TaskNodeID("id-node"), + WithTaskSpec(TaskImage("myimage:mytag")), + TaskDesiredState(swarm.TaskStateReady), + WithStatus(TaskState(swarm.TaskStateFailed), Timestamp(time.Now().Add(-2*time.Hour))), + )}, nil + }, + nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) { + return *Node(NodeName("node-name-bar")), nil, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newPsCommand(cli) + cmd.SetArgs([]string{"foo"}) + assert.NoError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "stack-ps-without-format.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} diff --git a/components/cli/cli/command/stack/remove_test.go b/components/cli/cli/command/stack/remove_test.go index d6efb90106..edc137041b 100644 --- a/components/cli/cli/command/stack/remove_test.go +++ b/components/cli/cli/command/stack/remove_test.go @@ -86,7 +86,7 @@ func TestSkipEmptyStack(t *testing.T) { assert.Equal(t, allConfigIDs, cli.removedConfigs) } -func TestContinueAfterError(t *testing.T) { +func TestRemoveContinueAfterError(t *testing.T) { allServices := []string{objectName("foo", "service1"), objectName("bar", "service1")} allServiceIDs := buildObjectIDs(allServices) diff --git a/components/cli/cli/command/stack/services.go b/components/cli/cli/command/stack/services.go index 459d7dd5a2..5b59c479c6 100644 --- a/components/cli/cli/command/stack/services.go +++ b/components/cli/cli/command/stack/services.go @@ -21,7 +21,7 @@ type servicesOptions struct { namespace string } -func newServicesCommand(dockerCli *command.DockerCli) *cobra.Command { +func newServicesCommand(dockerCli command.Cli) *cobra.Command { options := servicesOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -41,7 +41,7 @@ func newServicesCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runServices(dockerCli *command.DockerCli, options servicesOptions) error { +func runServices(dockerCli command.Cli, options servicesOptions) error { ctx := context.Background() client := dockerCli.Client() diff --git a/components/cli/cli/command/stack/services_test.go b/components/cli/cli/command/stack/services_test.go new file mode 100644 index 0000000000..a87174a14d --- /dev/null +++ b/components/cli/cli/command/stack/services_test.go @@ -0,0 +1,180 @@ +package stack + +import ( + "bytes" + "io/ioutil" + "testing" + + "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/cli/cli/internal/test/builders" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/pkg/testutil" + "github.com/docker/docker/pkg/testutil/golden" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestStackServicesErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + serviceListFunc func(options types.ServiceListOptions) ([]swarm.Service, error) + nodeListFunc func(options types.NodeListOptions) ([]swarm.Node, error) + taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error) + expectedError string + }{ + { + args: []string{"foo"}, + serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { + return nil, errors.Errorf("error getting services") + }, + expectedError: "error getting services", + }, + { + args: []string{"foo"}, + serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { + return []swarm.Service{*Service()}, nil + }, + nodeListFunc: func(options types.NodeListOptions) ([]swarm.Node, error) { + return nil, errors.Errorf("error getting nodes") + }, + expectedError: "error getting nodes", + }, + { + args: []string{"foo"}, + serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { + return []swarm.Service{*Service()}, nil + }, + taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { + return nil, errors.Errorf("error getting tasks") + }, + expectedError: "error getting tasks", + }, + { + args: []string{"foo"}, + flags: map[string]string{ + "format": "{{invalid format}}", + }, + serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { + return []swarm.Service{*Service()}, nil + }, + expectedError: "Template parsing error", + }, + } + + for _, tc := range testCases { + cli := test.NewFakeCli(&fakeClient{ + serviceListFunc: tc.serviceListFunc, + nodeListFunc: tc.nodeListFunc, + taskListFunc: tc.taskListFunc, + }, &bytes.Buffer{}) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newServicesCommand(cli) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) + } +} + +func TestStackServicesEmptyServiceList(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newServicesCommand( + test.NewFakeCli(&fakeClient{ + serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { + return []swarm.Service{}, nil + }, + }, buf), + ) + cmd.SetArgs([]string{"foo"}) + assert.NoError(t, cmd.Execute()) + testutil.EqualNormalizedString(t, testutil.RemoveSpace, buf.String(), "Nothing found in stack: foo") +} + +func TestStackServicesWithQuietOption(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { + return []swarm.Service{*Service(ServiceID("id-foo"))}, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newServicesCommand(cli) + cmd.Flags().Set("quiet", "true") + cmd.SetArgs([]string{"foo"}) + assert.NoError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "stack-services-with-quiet-option.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} + +func TestStackServicesWithFormat(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { + return []swarm.Service{ + *Service(ServiceName("service-name-foo")), + }, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newServicesCommand(cli) + cmd.SetArgs([]string{"foo"}) + cmd.Flags().Set("format", "{{ .Name }}") + assert.NoError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "stack-services-with-format.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} + +func TestStackServicesWithConfigFormat(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { + return []swarm.Service{ + *Service(ServiceName("service-name-foo")), + }, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{ + ServicesFormat: "{{ .Name }}", + }) + cmd := newServicesCommand(cli) + cmd.SetArgs([]string{"foo"}) + assert.NoError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "stack-services-with-config-format.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} + +func TestStackServicesWithoutFormat(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { + return []swarm.Service{*Service( + ServiceName("name-foo"), + ServiceID("id-foo"), + ReplicatedService(2), + ServiceImage("busybox:latest"), + ServicePort(swarm.PortConfig{ + PublishMode: swarm.PortConfigPublishModeIngress, + PublishedPort: 0, + TargetPort: 3232, + Protocol: swarm.PortConfigProtocolTCP, + }), + )}, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newServicesCommand(cli) + cmd.SetArgs([]string{"foo"}) + assert.NoError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "stack-services-without-format.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} diff --git a/components/cli/cli/command/stack/testdata/bundlefile_with_two_services.dab b/components/cli/cli/command/stack/testdata/bundlefile_with_two_services.dab new file mode 100644 index 0000000000..ced8180dc0 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/bundlefile_with_two_services.dab @@ -0,0 +1,29 @@ +{ + "Services": { + "visualizer": { + "Image": "busybox@sha256:32f093055929dbc23dec4d03e09dfe971f5973a9ca5cf059cbfb644c206aa83f", + "Networks": [ + "webnet" + ], + "Ports": [ + { + "Port": 8080, + "Protocol": "tcp" + } + ] + }, + "web": { + "Image": "busybox@sha256:32f093055929dbc23dec4d03e09dfe971f5973a9ca5cf059cbfb644c206aa83f", + "Networks": [ + "webnet" + ], + "Ports": [ + { + "Port": 80, + "Protocol": "tcp" + } + ] + } + }, + "Version": "0.1" +} diff --git a/components/cli/cli/command/stack/testdata/stack-list-sort.golden b/components/cli/cli/command/stack/testdata/stack-list-sort.golden new file mode 100644 index 0000000000..6bd116ebc6 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-list-sort.golden @@ -0,0 +1,3 @@ +NAME SERVICES +service-name-bar 1 +service-name-foo 1 diff --git a/components/cli/cli/command/stack/testdata/stack-list-with-format.golden b/components/cli/cli/command/stack/testdata/stack-list-with-format.golden new file mode 100644 index 0000000000..b53e6401f7 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-list-with-format.golden @@ -0,0 +1 @@ +service-name-foo diff --git a/components/cli/cli/command/stack/testdata/stack-list-without-format.golden b/components/cli/cli/command/stack/testdata/stack-list-without-format.golden new file mode 100644 index 0000000000..1b255654df --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-list-without-format.golden @@ -0,0 +1,2 @@ +NAME SERVICES +service-name-foo 1 diff --git a/components/cli/cli/command/stack/testdata/stack-ps-with-config-format.golden b/components/cli/cli/command/stack/testdata/stack-ps-with-config-format.golden new file mode 100644 index 0000000000..9ecebdafe3 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-ps-with-config-format.golden @@ -0,0 +1 @@ +service-id-foo.1 diff --git a/components/cli/cli/command/stack/testdata/stack-ps-with-format.golden b/components/cli/cli/command/stack/testdata/stack-ps-with-format.golden new file mode 100644 index 0000000000..9ecebdafe3 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-ps-with-format.golden @@ -0,0 +1 @@ +service-id-foo.1 diff --git a/components/cli/cli/command/stack/testdata/stack-ps-with-no-resolve-option.golden b/components/cli/cli/command/stack/testdata/stack-ps-with-no-resolve-option.golden new file mode 100644 index 0000000000..b90d743b8a --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-ps-with-no-resolve-option.golden @@ -0,0 +1 @@ +id-node-foo diff --git a/components/cli/cli/command/stack/testdata/stack-ps-with-no-trunc-option.golden b/components/cli/cli/command/stack/testdata/stack-ps-with-no-trunc-option.golden new file mode 100644 index 0000000000..8179bf4d65 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-ps-with-no-trunc-option.golden @@ -0,0 +1 @@ +xn4cypcov06f2w8gsbaf2lst3 diff --git a/components/cli/cli/command/stack/testdata/stack-ps-with-quiet-option.golden b/components/cli/cli/command/stack/testdata/stack-ps-with-quiet-option.golden new file mode 100644 index 0000000000..e2faeb6067 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-ps-with-quiet-option.golden @@ -0,0 +1 @@ +id-foo diff --git a/components/cli/cli/command/stack/testdata/stack-ps-without-format.golden b/components/cli/cli/command/stack/testdata/stack-ps-without-format.golden new file mode 100644 index 0000000000..9ca75f5890 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-ps-without-format.golden @@ -0,0 +1,2 @@ +ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS +id-foo service-id-foo.1 myimage:mytag node-name-bar Ready Failed 2 hours ago diff --git a/components/cli/cli/command/stack/testdata/stack-services-with-config-format.golden b/components/cli/cli/command/stack/testdata/stack-services-with-config-format.golden new file mode 100644 index 0000000000..b53e6401f7 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-services-with-config-format.golden @@ -0,0 +1 @@ +service-name-foo diff --git a/components/cli/cli/command/stack/testdata/stack-services-with-format.golden b/components/cli/cli/command/stack/testdata/stack-services-with-format.golden new file mode 100644 index 0000000000..b53e6401f7 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-services-with-format.golden @@ -0,0 +1 @@ +service-name-foo diff --git a/components/cli/cli/command/stack/testdata/stack-services-with-quiet-option.golden b/components/cli/cli/command/stack/testdata/stack-services-with-quiet-option.golden new file mode 100644 index 0000000000..e2faeb6067 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-services-with-quiet-option.golden @@ -0,0 +1 @@ +id-foo diff --git a/components/cli/cli/command/stack/testdata/stack-services-without-format.golden b/components/cli/cli/command/stack/testdata/stack-services-without-format.golden new file mode 100644 index 0000000000..c892250c20 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-services-without-format.golden @@ -0,0 +1,2 @@ +ID NAME MODE REPLICAS IMAGE PORTS +id-foo name-foo replicated 0/2 busybox:latest *:0->3232/tcp diff --git a/components/cli/cli/internal/test/builders/service.go b/components/cli/cli/internal/test/builders/service.go index dee91f4baa..71718268e1 100644 --- a/components/cli/cli/internal/test/builders/service.go +++ b/components/cli/cli/internal/test/builders/service.go @@ -14,6 +14,7 @@ func Service(builders ...func(*swarm.Service)) *swarm.Service { Annotations: swarm.Annotations{ Name: "defaultServiceName", }, + EndpointSpec: &swarm.EndpointSpec{}, }, } @@ -24,9 +25,44 @@ func Service(builders ...func(*swarm.Service)) *swarm.Service { return service } +// ServiceID sets the service ID +func ServiceID(ID string) func(*swarm.Service) { + return func(service *swarm.Service) { + service.ID = ID + } +} + // ServiceName sets the service name func ServiceName(name string) func(*swarm.Service) { return func(service *swarm.Service) { service.Spec.Annotations.Name = name } } + +// ServiceLabels sets the service's labels +func ServiceLabels(labels map[string]string) func(*swarm.Service) { + return func(service *swarm.Service) { + service.Spec.Annotations.Labels = labels + } +} + +// ReplicatedService sets the number of replicas for the service +func ReplicatedService(replicas uint64) func(*swarm.Service) { + return func(service *swarm.Service) { + service.Spec.Mode = swarm.ServiceMode{Replicated: &swarm.ReplicatedService{Replicas: &replicas}} + } +} + +// ServiceImage sets the service's image +func ServiceImage(image string) func(*swarm.Service) { + return func(service *swarm.Service) { + service.Spec.TaskTemplate = swarm.TaskSpec{ContainerSpec: swarm.ContainerSpec{Image: image}} + } +} + +// ServicePort sets the service's port +func ServicePort(port swarm.PortConfig) func(*swarm.Service) { + return func(service *swarm.Service) { + service.Spec.EndpointSpec.Ports = append(service.Spec.EndpointSpec.Ports, port) + } +} diff --git a/components/cli/cli/internal/test/builders/task.go b/components/cli/cli/internal/test/builders/task.go index 688c62a3a8..b4551c983b 100644 --- a/components/cli/cli/internal/test/builders/task.go +++ b/components/cli/cli/internal/test/builders/task.go @@ -42,13 +42,34 @@ func TaskID(id string) func(*swarm.Task) { } } -// ServiceID sets the task service's ID -func ServiceID(id string) func(*swarm.Task) { +// TaskName sets the task name +func TaskName(name string) func(*swarm.Task) { + return func(task *swarm.Task) { + task.Annotations.Name = name + } +} + +// TaskServiceID sets the task service's ID +func TaskServiceID(id string) func(*swarm.Task) { return func(task *swarm.Task) { task.ServiceID = id } } +// TaskNodeID sets the task's node id +func TaskNodeID(id string) func(*swarm.Task) { + return func(task *swarm.Task) { + task.NodeID = id + } +} + +// TaskDesiredState sets the task's desired state +func TaskDesiredState(state swarm.TaskState) func(*swarm.Task) { + return func(task *swarm.Task) { + task.DesiredState = state + } +} + // WithStatus sets the task status func WithStatus(statusBuilders ...func(*swarm.TaskStatus)) func(*swarm.Task) { return func(task *swarm.Task) { @@ -86,6 +107,13 @@ func StatusErr(err string) func(*swarm.TaskStatus) { } } +// TaskState sets the task's current state +func TaskState(state swarm.TaskState) func(*swarm.TaskStatus) { + return func(taskStatus *swarm.TaskStatus) { + taskStatus.State = state + } +} + // PortStatus sets the tasks port config status // FIXME(vdemeester) should be a sub builder 👼 func PortStatus(portConfigs []swarm.PortConfig) func(*swarm.TaskStatus) { @@ -94,6 +122,13 @@ func PortStatus(portConfigs []swarm.PortConfig) func(*swarm.TaskStatus) { } } +// WithTaskSpec sets the task spec +func WithTaskSpec(specBuilders ...func(*swarm.TaskSpec)) func(*swarm.Task) { + return func(task *swarm.Task) { + task.Spec = *TaskSpec(specBuilders...) + } +} + // TaskSpec creates a task spec with default values . // Any number of taskSpec function builder can be pass to augment it. func TaskSpec(specBuilders ...func(*swarm.TaskSpec)) *swarm.TaskSpec { @@ -109,3 +144,10 @@ func TaskSpec(specBuilders ...func(*swarm.TaskSpec)) *swarm.TaskSpec { return taskSpec } + +// TaskImage sets the task's image +func TaskImage(image string) func(*swarm.TaskSpec) { + return func(taskSpec *swarm.TaskSpec) { + taskSpec.ContainerSpec.Image = image + } +} From 5dbe00ec7cf7c1615c66083c6be9f2f531e44f04 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 5 Jul 2017 13:32:54 -0400 Subject: [PATCH 3/4] Fix stream for 'nothing found in stack' message Signed-off-by: Daniel Nephin (cherry picked from commit fb6deb107791adef1477455b4e3a77a4e65066d9) Signed-off-by: Sebastiaan van Stijn --- components/cli/cli/command/stack/ps.go | 2 +- components/cli/cli/command/stack/ps_test.go | 13 +++++++---- components/cli/cli/command/stack/remove.go | 2 +- .../cli/cli/command/stack/remove_test.go | 22 +++++++++++-------- components/cli/cli/command/stack/services.go | 4 +--- .../cli/cli/command/stack/services_test.go | 20 +++++++++-------- 6 files changed, 36 insertions(+), 27 deletions(-) diff --git a/components/cli/cli/command/stack/ps.go b/components/cli/cli/command/stack/ps.go index ae9ed0f70a..985d6ee8d8 100644 --- a/components/cli/cli/command/stack/ps.go +++ b/components/cli/cli/command/stack/ps.go @@ -58,7 +58,7 @@ func runPS(dockerCli command.Cli, options psOptions) error { } if len(tasks) == 0 { - fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace) + fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", namespace) return nil } diff --git a/components/cli/cli/command/stack/ps_test.go b/components/cli/cli/command/stack/ps_test.go index afda419eb4..7dc766609c 100644 --- a/components/cli/cli/command/stack/ps_test.go +++ b/components/cli/cli/command/stack/ps_test.go @@ -53,15 +53,20 @@ func TestStackPsErrors(t *testing.T) { } func TestStackPsEmptyStack(t *testing.T) { - buf := new(bytes.Buffer) - cmd := newPsCommand(test.NewFakeCli(&fakeClient{ + out := new(bytes.Buffer) + stderr := new(bytes.Buffer) + fakeCli := test.NewFakeCli(&fakeClient{ taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { return []swarm.Task{}, nil }, - }, buf)) + }, out) + fakeCli.SetErr(stderr) + cmd := newPsCommand(fakeCli) cmd.SetArgs([]string{"foo"}) + assert.NoError(t, cmd.Execute()) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, buf.String(), "Nothing found in stack: foo") + assert.Equal(t, "", out.String()) + assert.Equal(t, "Nothing found in stack: foo\n", stderr.String()) } func TestStackPsWithQuietOption(t *testing.T) { diff --git a/components/cli/cli/command/stack/remove.go b/components/cli/cli/command/stack/remove.go index 20d3ee71db..10a651cc13 100644 --- a/components/cli/cli/command/stack/remove.go +++ b/components/cli/cli/command/stack/remove.go @@ -72,7 +72,7 @@ func runRemove(dockerCli command.Cli, opts removeOptions) error { } if len(services)+len(networks)+len(secrets)+len(configs) == 0 { - fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace) + fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", namespace) continue } diff --git a/components/cli/cli/command/stack/remove_test.go b/components/cli/cli/command/stack/remove_test.go index edc137041b..1e489030b4 100644 --- a/components/cli/cli/command/stack/remove_test.go +++ b/components/cli/cli/command/stack/remove_test.go @@ -55,8 +55,9 @@ func TestRemoveStack(t *testing.T) { assert.Equal(t, allConfigIDs, cli.removedConfigs) } -func TestSkipEmptyStack(t *testing.T) { - buf := new(bytes.Buffer) +func TestRemoveStackSkipEmpty(t *testing.T) { + out := new(bytes.Buffer) + stderr := new(bytes.Buffer) allServices := []string{objectName("bar", "service1"), objectName("bar", "service2")} allServiceIDs := buildObjectIDs(allServices) @@ -69,21 +70,24 @@ func TestSkipEmptyStack(t *testing.T) { allConfigs := []string{objectName("bar", "config1")} allConfigIDs := buildObjectIDs(allConfigs) - cli := &fakeClient{ + fakeClient := &fakeClient{ services: allServices, networks: allNetworks, secrets: allSecrets, configs: allConfigs, } - cmd := newRemoveCommand(test.NewFakeCli(cli, buf)) + fakeCli := test.NewFakeCli(fakeClient, out) + fakeCli.SetErr(stderr) + cmd := newRemoveCommand(fakeCli) cmd.SetArgs([]string{"foo", "bar"}) assert.NoError(t, cmd.Execute()) - assert.Contains(t, buf.String(), "Nothing found in stack: foo") - assert.Equal(t, allServiceIDs, cli.removedServices) - assert.Equal(t, allNetworkIDs, cli.removedNetworks) - assert.Equal(t, allSecretIDs, cli.removedSecrets) - assert.Equal(t, allConfigIDs, cli.removedConfigs) + assert.Equal(t, "", out.String()) + assert.Contains(t, stderr.String(), "Nothing found in stack: foo\n") + assert.Equal(t, allServiceIDs, fakeClient.removedServices) + assert.Equal(t, allNetworkIDs, fakeClient.removedNetworks) + assert.Equal(t, allSecretIDs, fakeClient.removedSecrets) + assert.Equal(t, allConfigIDs, fakeClient.removedConfigs) } func TestRemoveContinueAfterError(t *testing.T) { diff --git a/components/cli/cli/command/stack/services.go b/components/cli/cli/command/stack/services.go index 5b59c479c6..94fcb8bded 100644 --- a/components/cli/cli/command/stack/services.go +++ b/components/cli/cli/command/stack/services.go @@ -51,11 +51,9 @@ func runServices(dockerCli command.Cli, options servicesOptions) error { return err } - out := dockerCli.Out() - // if no services in this stack, print message and exit 0 if len(services) == 0 { - fmt.Fprintf(out, "Nothing found in stack: %s\n", options.namespace) + fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", options.namespace) return nil } diff --git a/components/cli/cli/command/stack/services_test.go b/components/cli/cli/command/stack/services_test.go index a87174a14d..88408b4cd3 100644 --- a/components/cli/cli/command/stack/services_test.go +++ b/components/cli/cli/command/stack/services_test.go @@ -83,17 +83,19 @@ func TestStackServicesErrors(t *testing.T) { } func TestStackServicesEmptyServiceList(t *testing.T) { - buf := new(bytes.Buffer) - cmd := newServicesCommand( - test.NewFakeCli(&fakeClient{ - serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{}, nil - }, - }, buf), - ) + out := new(bytes.Buffer) + stderr := new(bytes.Buffer) + fakeCli := test.NewFakeCli(&fakeClient{ + serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { + return []swarm.Service{}, nil + }, + }, out) + fakeCli.SetErr(stderr) + cmd := newServicesCommand(fakeCli) cmd.SetArgs([]string{"foo"}) assert.NoError(t, cmd.Execute()) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, buf.String(), "Nothing found in stack: foo") + assert.Equal(t, "", out.String()) + assert.Equal(t, "Nothing found in stack: foo\n", stderr.String()) } func TestStackServicesWithQuietOption(t *testing.T) { From a703a55a77a770fba039b012065ddf24bc170f06 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 30 Jun 2017 17:00:16 -0700 Subject: [PATCH 4/4] Don't attempt to remove unsupported resources on older daemon When running `docker stack rm ` against an older daemon, a warning was printed for "configs" being ignored; WARNING: ignoring "configs" (requires API version 1.30, but the Docker daemon API version is 1.26) Given that an old daemon cannot _have_ configs, there should not be a need to warn, or _attempt_ to remove these resources. This patch removes the warning, and skips fetching (and removing) configs. A check if _secrets_ are supported by the daemon is also added, given that this would result in an error when attempted against an older (pre 1.13) daemon. There is one situation where this could lead to secrets or configs being left behind; if the client is connecting to a daemon that _does_ support secrets, configs, but the API version is overridden using `DOCKER_API_VERSION`, no warning is printed, and secrets and configs are not attempted to be removed. Given that `DOCKER_API_VERSION` is regarded a feature for debugging / "power users", it should be ok to ignore this. Signed-off-by: Sebastiaan van Stijn (cherry picked from commit 2429f15672eaa617f3fbcf73692d660fe63315ba) Signed-off-by: Sebastiaan van Stijn --- .../cli/cli/command/stack/client_test.go | 6 +++ components/cli/cli/command/stack/remove.go | 18 ++++----- .../cli/cli/command/stack/remove_test.go | 38 +++++++++++++++++++ 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/components/cli/cli/command/stack/client_test.go b/components/cli/cli/command/stack/client_test.go index 50442783fa..d1d85193e7 100644 --- a/components/cli/cli/command/stack/client_test.go +++ b/components/cli/cli/command/stack/client_test.go @@ -15,6 +15,8 @@ import ( type fakeClient struct { client.Client + version string + services []string networks []string secrets []string @@ -45,6 +47,10 @@ func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) }, nil } +func (cli *fakeClient) ClientVersion() string { + return cli.version +} + func (cli *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) { if cli.serviceListFunc != nil { return cli.serviceListFunc(options) diff --git a/components/cli/cli/command/stack/remove.go b/components/cli/cli/command/stack/remove.go index 10a651cc13..a6cbcef0d6 100644 --- a/components/cli/cli/command/stack/remove.go +++ b/components/cli/cli/command/stack/remove.go @@ -51,20 +51,16 @@ func runRemove(dockerCli command.Cli, opts removeOptions) error { return err } - secrets, err := getStackSecrets(ctx, client, namespace) - if err != nil { - return err + var secrets []swarm.Secret + if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.25") { + secrets, err = getStackSecrets(ctx, client, namespace) + if err != nil { + return err + } } var configs []swarm.Config - - version, err := client.ServerVersion(ctx) - if err != nil { - return err - } - if versions.LessThan(version.APIVersion, "1.30") { - fmt.Fprintf(dockerCli.Err(), "WARNING: ignoring \"configs\" (requires API version 1.30, but the Docker daemon API version is %s)\n", version.APIVersion) - } else { + if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.30") { configs, err = getStackConfigs(ctx, client, namespace) if err != nil { return err diff --git a/components/cli/cli/command/stack/remove_test.go b/components/cli/cli/command/stack/remove_test.go index 1e489030b4..61b01b7c21 100644 --- a/components/cli/cli/command/stack/remove_test.go +++ b/components/cli/cli/command/stack/remove_test.go @@ -39,7 +39,9 @@ func TestRemoveStack(t *testing.T) { } allConfigIDs := buildObjectIDs(allConfigs) + // Using API 1.24; removes services, networks, but doesn't remove configs and secrets cli := &fakeClient{ + version: "1.24", services: allServices, networks: allNetworks, secrets: allSecrets, @@ -48,6 +50,40 @@ func TestRemoveStack(t *testing.T) { cmd := newRemoveCommand(test.NewFakeCli(cli, &bytes.Buffer{})) cmd.SetArgs([]string{"foo", "bar"}) + assert.NoError(t, cmd.Execute()) + assert.Equal(t, allServiceIDs, cli.removedServices) + assert.Equal(t, allNetworkIDs, cli.removedNetworks) + assert.Nil(t, cli.removedSecrets) + assert.Nil(t, cli.removedConfigs) + + // Using API 1.25; removes services, networks, but doesn't remove configs + cli = &fakeClient{ + version: "1.25", + services: allServices, + networks: allNetworks, + secrets: allSecrets, + configs: allConfigs, + } + cmd = newRemoveCommand(test.NewFakeCli(cli, &bytes.Buffer{})) + cmd.SetArgs([]string{"foo", "bar"}) + + assert.NoError(t, cmd.Execute()) + assert.Equal(t, allServiceIDs, cli.removedServices) + assert.Equal(t, allNetworkIDs, cli.removedNetworks) + assert.Equal(t, allSecretIDs, cli.removedSecrets) + assert.Nil(t, cli.removedConfigs) + + // Using API 1.30; removes services, networks, configs, and secrets + cli = &fakeClient{ + version: "1.30", + services: allServices, + networks: allNetworks, + secrets: allSecrets, + configs: allConfigs, + } + cmd = newRemoveCommand(test.NewFakeCli(cli, &bytes.Buffer{})) + cmd.SetArgs([]string{"foo", "bar"}) + assert.NoError(t, cmd.Execute()) assert.Equal(t, allServiceIDs, cli.removedServices) assert.Equal(t, allNetworkIDs, cli.removedNetworks) @@ -71,6 +107,7 @@ func TestRemoveStackSkipEmpty(t *testing.T) { allConfigIDs := buildObjectIDs(allConfigs) fakeClient := &fakeClient{ + version: "1.30", services: allServices, networks: allNetworks, secrets: allSecrets, @@ -105,6 +142,7 @@ func TestRemoveContinueAfterError(t *testing.T) { removedServices := []string{} cli := &fakeClient{ + version: "1.30", services: allServices, networks: allNetworks, secrets: allSecrets,