diff --git a/cli/command/node/ps_test.go b/cli/command/node/ps_test.go index ddc0eba0d4..d25a55f0cf 100644 --- a/cli/command/node/ps_test.go +++ b/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/cli/command/stack/client_test.go b/cli/command/stack/client_test.go index 22be9dc06a..50442783fa 100644 --- a/cli/command/stack/client_test.go +++ b/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/cli/command/stack/list.go b/cli/command/stack/list.go index 4560a98ec9..a14af0ad21 100644 --- a/cli/command/stack/list.go +++ b/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/cli/command/stack/list_test.go b/cli/command/stack/list_test.go new file mode 100644 index 0000000000..4f258977ed --- /dev/null +++ b/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/cli/command/stack/opts_test.go b/cli/command/stack/opts_test.go new file mode 100644 index 0000000000..b57dcd89f6 --- /dev/null +++ b/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/cli/command/stack/ps_test.go b/cli/command/stack/ps_test.go new file mode 100644 index 0000000000..afda419eb4 --- /dev/null +++ b/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/cli/command/stack/remove_test.go b/cli/command/stack/remove_test.go index 05092d84ac..498416ebe8 100644 --- a/cli/command/stack/remove_test.go +++ b/cli/command/stack/remove_test.go @@ -87,7 +87,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/cli/command/stack/services.go b/cli/command/stack/services.go index 459d7dd5a2..5b59c479c6 100644 --- a/cli/command/stack/services.go +++ b/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/cli/command/stack/services_test.go b/cli/command/stack/services_test.go new file mode 100644 index 0000000000..a87174a14d --- /dev/null +++ b/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/cli/command/stack/testdata/bundlefile_with_two_services.dab b/cli/command/stack/testdata/bundlefile_with_two_services.dab new file mode 100644 index 0000000000..ced8180dc0 --- /dev/null +++ b/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/cli/command/stack/testdata/stack-list-sort.golden b/cli/command/stack/testdata/stack-list-sort.golden new file mode 100644 index 0000000000..6bd116ebc6 --- /dev/null +++ b/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/cli/command/stack/testdata/stack-list-with-format.golden b/cli/command/stack/testdata/stack-list-with-format.golden new file mode 100644 index 0000000000..b53e6401f7 --- /dev/null +++ b/cli/command/stack/testdata/stack-list-with-format.golden @@ -0,0 +1 @@ +service-name-foo diff --git a/cli/command/stack/testdata/stack-list-without-format.golden b/cli/command/stack/testdata/stack-list-without-format.golden new file mode 100644 index 0000000000..1b255654df --- /dev/null +++ b/cli/command/stack/testdata/stack-list-without-format.golden @@ -0,0 +1,2 @@ +NAME SERVICES +service-name-foo 1 diff --git a/cli/command/stack/testdata/stack-ps-with-config-format.golden b/cli/command/stack/testdata/stack-ps-with-config-format.golden new file mode 100644 index 0000000000..9ecebdafe3 --- /dev/null +++ b/cli/command/stack/testdata/stack-ps-with-config-format.golden @@ -0,0 +1 @@ +service-id-foo.1 diff --git a/cli/command/stack/testdata/stack-ps-with-format.golden b/cli/command/stack/testdata/stack-ps-with-format.golden new file mode 100644 index 0000000000..9ecebdafe3 --- /dev/null +++ b/cli/command/stack/testdata/stack-ps-with-format.golden @@ -0,0 +1 @@ +service-id-foo.1 diff --git a/cli/command/stack/testdata/stack-ps-with-no-resolve-option.golden b/cli/command/stack/testdata/stack-ps-with-no-resolve-option.golden new file mode 100644 index 0000000000..b90d743b8a --- /dev/null +++ b/cli/command/stack/testdata/stack-ps-with-no-resolve-option.golden @@ -0,0 +1 @@ +id-node-foo diff --git a/cli/command/stack/testdata/stack-ps-with-no-trunc-option.golden b/cli/command/stack/testdata/stack-ps-with-no-trunc-option.golden new file mode 100644 index 0000000000..8179bf4d65 --- /dev/null +++ b/cli/command/stack/testdata/stack-ps-with-no-trunc-option.golden @@ -0,0 +1 @@ +xn4cypcov06f2w8gsbaf2lst3 diff --git a/cli/command/stack/testdata/stack-ps-with-quiet-option.golden b/cli/command/stack/testdata/stack-ps-with-quiet-option.golden new file mode 100644 index 0000000000..e2faeb6067 --- /dev/null +++ b/cli/command/stack/testdata/stack-ps-with-quiet-option.golden @@ -0,0 +1 @@ +id-foo diff --git a/cli/command/stack/testdata/stack-ps-without-format.golden b/cli/command/stack/testdata/stack-ps-without-format.golden new file mode 100644 index 0000000000..9ca75f5890 --- /dev/null +++ b/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/cli/command/stack/testdata/stack-services-with-config-format.golden b/cli/command/stack/testdata/stack-services-with-config-format.golden new file mode 100644 index 0000000000..b53e6401f7 --- /dev/null +++ b/cli/command/stack/testdata/stack-services-with-config-format.golden @@ -0,0 +1 @@ +service-name-foo diff --git a/cli/command/stack/testdata/stack-services-with-format.golden b/cli/command/stack/testdata/stack-services-with-format.golden new file mode 100644 index 0000000000..b53e6401f7 --- /dev/null +++ b/cli/command/stack/testdata/stack-services-with-format.golden @@ -0,0 +1 @@ +service-name-foo diff --git a/cli/command/stack/testdata/stack-services-with-quiet-option.golden b/cli/command/stack/testdata/stack-services-with-quiet-option.golden new file mode 100644 index 0000000000..e2faeb6067 --- /dev/null +++ b/cli/command/stack/testdata/stack-services-with-quiet-option.golden @@ -0,0 +1 @@ +id-foo diff --git a/cli/command/stack/testdata/stack-services-without-format.golden b/cli/command/stack/testdata/stack-services-without-format.golden new file mode 100644 index 0000000000..c892250c20 --- /dev/null +++ b/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/cli/internal/test/builders/service.go b/cli/internal/test/builders/service.go index dee91f4baa..71718268e1 100644 --- a/cli/internal/test/builders/service.go +++ b/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/cli/internal/test/builders/task.go b/cli/internal/test/builders/task.go index 688c62a3a8..b4551c983b 100644 --- a/cli/internal/test/builders/task.go +++ b/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 + } +}