From f151c297eb268e22dc1eb36ded0e356885f40739 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Sun, 25 Dec 2016 22:23:35 +0100 Subject: [PATCH] Add some unit tests to the node and swarm cli code Start work on adding unit tests to our cli code in order to have to write less costly integration test. Signed-off-by: Vincent Demeester --- cli/command/cli.go | 10 +- cli/command/node/client_test.go | 68 ++++++ cli/command/node/demote.go | 4 +- cli/command/node/demote_test.go | 88 +++++++ cli/command/node/inspect.go | 4 +- cli/command/node/inspect_test.go | 122 ++++++++++ cli/command/node/list.go | 4 +- cli/command/node/list_test.go | 101 ++++++++ cli/command/node/opts.go | 36 --- cli/command/node/promote.go | 4 +- cli/command/node/promote_test.go | 88 +++++++ cli/command/node/ps.go | 4 +- cli/command/node/ps_test.go | 132 +++++++++++ cli/command/node/remove.go | 4 +- cli/command/node/remove_test.go | 47 ++++ .../node-inspect-pretty.manager-leader.golden | 25 ++ .../node-inspect-pretty.manager.golden | 25 ++ .../node-inspect-pretty.simple.golden | 23 ++ .../node/testdata/node-ps.simple.golden | 2 + .../node/testdata/node-ps.with-errors.golden | 4 + cli/command/node/update.go | 6 +- cli/command/node/update_test.go | 172 ++++++++++++++ cli/command/swarm/client_test.go | 84 +++++++ cli/command/swarm/init.go | 6 +- cli/command/swarm/init_test.go | 129 +++++++++++ cli/command/swarm/join.go | 4 +- cli/command/swarm/join_test.go | 102 +++++++++ cli/command/swarm/join_token.go | 6 +- cli/command/swarm/join_token_test.go | 215 ++++++++++++++++++ cli/command/swarm/leave.go | 4 +- cli/command/swarm/leave_test.go | 52 +++++ cli/command/swarm/opts_test.go | 73 ++++++ .../swarm/testdata/init-init-autolock.golden | 11 + cli/command/swarm/testdata/init-init.golden | 4 + .../testdata/jointoken-manager-quiet.golden | 1 + .../testdata/jointoken-manager-rotate.golden | 8 + .../swarm/testdata/jointoken-manager.golden | 6 + .../testdata/jointoken-worker-quiet.golden | 1 + .../swarm/testdata/jointoken-worker.golden | 6 + .../unlockkeys-unlock-key-quiet.golden | 1 + .../unlockkeys-unlock-key-rotate-quiet.golden | 1 + .../unlockkeys-unlock-key-rotate.golden | 9 + .../testdata/unlockkeys-unlock-key.golden | 7 + .../testdata/update-all-flags-quiet.golden | 1 + .../update-autolock-unlock-key.golden | 8 + .../swarm/testdata/update-noargs.golden | 13 ++ cli/command/swarm/unlock.go | 4 +- cli/command/swarm/unlock_key.go | 9 +- cli/command/swarm/unlock_key_test.go | 175 ++++++++++++++ cli/command/swarm/unlock_test.go | 101 ++++++++ cli/command/swarm/update.go | 14 +- cli/command/swarm/update_test.go | 182 +++++++++++++++ cli/command/task/print.go | 4 +- cli/internal/test/builders/node.go | 117 ++++++++++ cli/internal/test/builders/swarm.go | 39 ++++ cli/internal/test/builders/task.go | 111 +++++++++ cli/internal/test/cli.go | 48 ++++ integration-cli/docker_cli_swarm_test.go | 82 ++----- pkg/testutil/assert/assert.go | 20 ++ pkg/testutil/golden/golden.go | 28 +++ 60 files changed, 2512 insertions(+), 147 deletions(-) create mode 100644 cli/command/node/client_test.go create mode 100644 cli/command/node/demote_test.go create mode 100644 cli/command/node/inspect_test.go create mode 100644 cli/command/node/list_test.go create mode 100644 cli/command/node/promote_test.go create mode 100644 cli/command/node/ps_test.go create mode 100644 cli/command/node/remove_test.go create mode 100644 cli/command/node/testdata/node-inspect-pretty.manager-leader.golden create mode 100644 cli/command/node/testdata/node-inspect-pretty.manager.golden create mode 100644 cli/command/node/testdata/node-inspect-pretty.simple.golden create mode 100644 cli/command/node/testdata/node-ps.simple.golden create mode 100644 cli/command/node/testdata/node-ps.with-errors.golden create mode 100644 cli/command/node/update_test.go create mode 100644 cli/command/swarm/client_test.go create mode 100644 cli/command/swarm/init_test.go create mode 100644 cli/command/swarm/join_test.go create mode 100644 cli/command/swarm/join_token_test.go create mode 100644 cli/command/swarm/leave_test.go create mode 100644 cli/command/swarm/testdata/init-init-autolock.golden create mode 100644 cli/command/swarm/testdata/init-init.golden create mode 100644 cli/command/swarm/testdata/jointoken-manager-quiet.golden create mode 100644 cli/command/swarm/testdata/jointoken-manager-rotate.golden create mode 100644 cli/command/swarm/testdata/jointoken-manager.golden create mode 100644 cli/command/swarm/testdata/jointoken-worker-quiet.golden create mode 100644 cli/command/swarm/testdata/jointoken-worker.golden create mode 100644 cli/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden create mode 100644 cli/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden create mode 100644 cli/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden create mode 100644 cli/command/swarm/testdata/unlockkeys-unlock-key.golden create mode 100644 cli/command/swarm/testdata/update-all-flags-quiet.golden create mode 100644 cli/command/swarm/testdata/update-autolock-unlock-key.golden create mode 100644 cli/command/swarm/testdata/update-noargs.golden create mode 100644 cli/command/swarm/unlock_key_test.go create mode 100644 cli/command/swarm/unlock_test.go create mode 100644 cli/command/swarm/update_test.go create mode 100644 cli/internal/test/builders/node.go create mode 100644 cli/internal/test/builders/swarm.go create mode 100644 cli/internal/test/builders/task.go create mode 100644 cli/internal/test/cli.go create mode 100644 pkg/testutil/golden/golden.go diff --git a/cli/command/cli.go b/cli/command/cli.go index c287ebcf77..bf9d554608 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -32,7 +32,15 @@ type Streams interface { Err() io.Writer } -// DockerCli represents the docker command line client. +// Cli represents the docker command line client. +type Cli interface { + Client() client.APIClient + Out() *OutStream + Err() io.Writer + In() *InStream +} + +// DockerCli is an instance the docker command line client. // Instances of the client can be returned from NewDockerCli. type DockerCli struct { configFile *configfile.ConfigFile diff --git a/cli/command/node/client_test.go b/cli/command/node/client_test.go new file mode 100644 index 0000000000..1f5cdc7cee --- /dev/null +++ b/cli/command/node/client_test.go @@ -0,0 +1,68 @@ +package node + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" + "golang.org/x/net/context" +) + +type fakeClient struct { + client.Client + infoFunc func() (types.Info, error) + nodeInspectFunc func() (swarm.Node, []byte, error) + nodeListFunc func() ([]swarm.Node, error) + nodeRemoveFunc func() error + nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error + taskInspectFunc func(taskID string) (swarm.Task, []byte, error) + taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error) +} + +func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, ref string) (swarm.Node, []byte, error) { + if cli.nodeInspectFunc != nil { + return cli.nodeInspectFunc() + } + return swarm.Node{}, []byte{}, nil +} + +func (cli *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) { + if cli.nodeListFunc != nil { + return cli.nodeListFunc() + } + return []swarm.Node{}, nil +} + +func (cli *fakeClient) NodeRemove(ctx context.Context, nodeID string, options types.NodeRemoveOptions) error { + if cli.nodeRemoveFunc != nil { + return cli.nodeRemoveFunc() + } + return nil +} + +func (cli *fakeClient) NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if cli.nodeUpdateFunc != nil { + return cli.nodeUpdateFunc(nodeID, version, node) + } + return nil +} + +func (cli *fakeClient) Info(ctx context.Context) (types.Info, error) { + if cli.infoFunc != nil { + return cli.infoFunc() + } + return types.Info{}, nil +} + +func (cli *fakeClient) TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) { + if cli.taskInspectFunc != nil { + return cli.taskInspectFunc(taskID) + } + return swarm.Task{}, []byte{}, 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 +} diff --git a/cli/command/node/demote.go b/cli/command/node/demote.go index 33f86c6499..72ed3ea630 100644 --- a/cli/command/node/demote.go +++ b/cli/command/node/demote.go @@ -9,7 +9,7 @@ import ( "github.com/spf13/cobra" ) -func newDemoteCommand(dockerCli *command.DockerCli) *cobra.Command { +func newDemoteCommand(dockerCli command.Cli) *cobra.Command { return &cobra.Command{ Use: "demote NODE [NODE...]", Short: "Demote one or more nodes from manager in the swarm", @@ -20,7 +20,7 @@ func newDemoteCommand(dockerCli *command.DockerCli) *cobra.Command { } } -func runDemote(dockerCli *command.DockerCli, nodes []string) error { +func runDemote(dockerCli command.Cli, nodes []string) error { demote := func(node *swarm.Node) error { if node.Spec.Role == swarm.NodeRoleWorker { fmt.Fprintf(dockerCli.Out(), "Node %s is already a worker.\n", node.ID) diff --git a/cli/command/node/demote_test.go b/cli/command/node/demote_test.go new file mode 100644 index 0000000000..3ba88f41c8 --- /dev/null +++ b/cli/command/node/demote_test.go @@ -0,0 +1,88 @@ +package node + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestNodeDemoteErrors(t *testing.T) { + testCases := []struct { + args []string + nodeInspectFunc func() (swarm.Node, []byte, error) + nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error + expectedError string + }{ + { + expectedError: "requires at least 1 argument", + }, + { + args: []string{"nodeID"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + }, + expectedError: "error inspecting the node", + }, + { + args: []string{"nodeID"}, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + return fmt.Errorf("error updating the node") + }, + expectedError: "error updating the node", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newDemoteCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: tc.nodeInspectFunc, + nodeUpdateFunc: tc.nodeUpdateFunc, + }, buf)) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNodeDemoteNoChange(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newDemoteCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if node.Role != swarm.NodeRoleWorker { + return fmt.Errorf("expected role worker, got %s", node.Role) + } + return nil + }, + }, buf)) + cmd.SetArgs([]string{"nodeID"}) + assert.NilError(t, cmd.Execute()) +} + +func TestNodeDemoteMultipleNode(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newDemoteCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager()), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if node.Role != swarm.NodeRoleWorker { + return fmt.Errorf("expected role worker, got %s", node.Role) + } + return nil + }, + }, buf)) + cmd.SetArgs([]string{"nodeID1", "nodeID2"}) + assert.NilError(t, cmd.Execute()) +} diff --git a/cli/command/node/inspect.go b/cli/command/node/inspect.go index fde70185f8..97a2717781 100644 --- a/cli/command/node/inspect.go +++ b/cli/command/node/inspect.go @@ -22,7 +22,7 @@ type inspectOptions struct { pretty bool } -func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { +func newInspectCommand(dockerCli command.Cli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ @@ -41,7 +41,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { +func runInspect(dockerCli command.Cli, opts inspectOptions) error { client := dockerCli.Client() ctx := context.Background() getRef := func(ref string) (interface{}, []byte, error) { diff --git a/cli/command/node/inspect_test.go b/cli/command/node/inspect_test.go new file mode 100644 index 0000000000..91bd41e165 --- /dev/null +++ b/cli/command/node/inspect_test.go @@ -0,0 +1,122 @@ +package node + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestNodeInspectErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + nodeInspectFunc func() (swarm.Node, []byte, error) + infoFunc func() (types.Info, error) + expectedError string + }{ + { + expectedError: "requires at least 1 argument", + }, + { + args: []string{"self"}, + infoFunc: func() (types.Info, error) { + return types.Info{}, fmt.Errorf("error asking for node info") + }, + expectedError: "error asking for node info", + }, + { + args: []string{"nodeID"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + }, + infoFunc: func() (types.Info, error) { + return types.Info{}, fmt.Errorf("error asking for node info") + }, + expectedError: "error inspecting the node", + }, + { + args: []string{"self"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + }, + infoFunc: func() (types.Info, error) { + return types.Info{}, nil + }, + expectedError: "error inspecting the node", + }, + { + args: []string{"self"}, + flags: map[string]string{ + "pretty": "true", + }, + infoFunc: func() (types.Info, error) { + return types.Info{}, fmt.Errorf("error asking for node info") + }, + expectedError: "error asking for node info", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newInspectCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: tc.nodeInspectFunc, + infoFunc: tc.infoFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNodeInspectPretty(t *testing.T) { + testCases := []struct { + name string + nodeInspectFunc func() (swarm.Node, []byte, error) + }{ + { + name: "simple", + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(NodeLabels(map[string]string{ + "lbl1": "value1", + })), []byte{}, nil + }, + }, + { + name: "manager", + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager()), []byte{}, nil + }, + }, + { + name: "manager-leader", + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager(Leader())), []byte{}, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newInspectCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: tc.nodeInspectFunc, + }, buf)) + cmd.SetArgs([]string{"nodeID"}) + cmd.Flags().Set("pretty", "true") + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("node-inspect-pretty.%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} diff --git a/cli/command/node/list.go b/cli/command/node/list.go index 9cacdcf441..d166401ab7 100644 --- a/cli/command/node/list.go +++ b/cli/command/node/list.go @@ -24,7 +24,7 @@ type listOptions struct { filter opts.FilterOpt } -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { +func newListCommand(dockerCli command.Cli) *cobra.Command { opts := listOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -43,7 +43,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() out := dockerCli.Out() ctx := context.Background() diff --git a/cli/command/node/list_test.go b/cli/command/node/list_test.go new file mode 100644 index 0000000000..237c4be9ca --- /dev/null +++ b/cli/command/node/list_test.go @@ -0,0 +1,101 @@ +package node + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestNodeListErrorOnAPIFailure(t *testing.T) { + testCases := []struct { + nodeListFunc func() ([]swarm.Node, error) + infoFunc func() (types.Info, error) + expectedError string + }{ + { + nodeListFunc: func() ([]swarm.Node, error) { + return []swarm.Node{}, fmt.Errorf("error listing nodes") + }, + expectedError: "error listing nodes", + }, + { + nodeListFunc: func() ([]swarm.Node, error) { + return []swarm.Node{ + { + ID: "nodeID", + }, + }, nil + }, + infoFunc: func() (types.Info, error) { + return types.Info{}, fmt.Errorf("error asking for node info") + }, + expectedError: "error asking for node info", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newListCommand( + test.NewFakeCli(&fakeClient{ + nodeListFunc: tc.nodeListFunc, + infoFunc: tc.infoFunc, + }, buf)) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNodeList(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newListCommand( + test.NewFakeCli(&fakeClient{ + nodeListFunc: func() ([]swarm.Node, error) { + return []swarm.Node{ + *Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())), + *Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()), + *Node(NodeID("nodeID3"), Hostname("nodeHostname3")), + }, nil + }, + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + NodeID: "nodeID1", + }, + }, nil + }, + }, buf)) + assert.NilError(t, cmd.Execute()) + assert.Contains(t, buf.String(), `nodeID1 * nodeHostname1 Ready Active Leader`) + assert.Contains(t, buf.String(), `nodeID2 nodeHostname2 Ready Active Reachable`) + assert.Contains(t, buf.String(), `nodeID3 nodeHostname3 Ready Active`) +} + +func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newListCommand( + test.NewFakeCli(&fakeClient{ + nodeListFunc: func() ([]swarm.Node, error) { + return []swarm.Node{ + *Node(), + }, nil + }, + }, buf)) + cmd.Flags().Set("quiet", "true") + assert.NilError(t, cmd.Execute()) + assert.Contains(t, buf.String(), "nodeID") +} + +// Test case for #24090 +func TestNodeListContainsHostname(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newListCommand(test.NewFakeCli(&fakeClient{}, buf)) + assert.NilError(t, cmd.Execute()) + assert.Contains(t, buf.String(), "HOSTNAME") +} diff --git a/cli/command/node/opts.go b/cli/command/node/opts.go index 7e6c55d487..0ad365f0c6 100644 --- a/cli/command/node/opts.go +++ b/cli/command/node/opts.go @@ -1,12 +1,7 @@ package node import ( - "fmt" - "strings" - - "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/opts" - runconfigopts "github.com/docker/docker/runconfig/opts" ) type nodeOptions struct { @@ -27,34 +22,3 @@ func newNodeOptions() *nodeOptions { }, } } - -func (opts *nodeOptions) ToNodeSpec() (swarm.NodeSpec, error) { - var spec swarm.NodeSpec - - spec.Annotations.Name = opts.annotations.name - spec.Annotations.Labels = runconfigopts.ConvertKVStringsToMap(opts.annotations.labels.GetAll()) - - switch swarm.NodeRole(strings.ToLower(opts.role)) { - case swarm.NodeRoleWorker: - spec.Role = swarm.NodeRoleWorker - case swarm.NodeRoleManager: - spec.Role = swarm.NodeRoleManager - case "": - default: - return swarm.NodeSpec{}, fmt.Errorf("invalid role %q, only worker and manager are supported", opts.role) - } - - switch swarm.NodeAvailability(strings.ToLower(opts.availability)) { - case swarm.NodeAvailabilityActive: - spec.Availability = swarm.NodeAvailabilityActive - case swarm.NodeAvailabilityPause: - spec.Availability = swarm.NodeAvailabilityPause - case swarm.NodeAvailabilityDrain: - spec.Availability = swarm.NodeAvailabilityDrain - case "": - default: - return swarm.NodeSpec{}, fmt.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) - } - - return spec, nil -} diff --git a/cli/command/node/promote.go b/cli/command/node/promote.go index f47d783f4c..94fff6400b 100644 --- a/cli/command/node/promote.go +++ b/cli/command/node/promote.go @@ -9,7 +9,7 @@ import ( "github.com/spf13/cobra" ) -func newPromoteCommand(dockerCli *command.DockerCli) *cobra.Command { +func newPromoteCommand(dockerCli command.Cli) *cobra.Command { return &cobra.Command{ Use: "promote NODE [NODE...]", Short: "Promote one or more nodes to manager in the swarm", @@ -20,7 +20,7 @@ func newPromoteCommand(dockerCli *command.DockerCli) *cobra.Command { } } -func runPromote(dockerCli *command.DockerCli, nodes []string) error { +func runPromote(dockerCli command.Cli, nodes []string) error { promote := func(node *swarm.Node) error { if node.Spec.Role == swarm.NodeRoleManager { fmt.Fprintf(dockerCli.Out(), "Node %s is already a manager.\n", node.ID) diff --git a/cli/command/node/promote_test.go b/cli/command/node/promote_test.go new file mode 100644 index 0000000000..ef4666321d --- /dev/null +++ b/cli/command/node/promote_test.go @@ -0,0 +1,88 @@ +package node + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestNodePromoteErrors(t *testing.T) { + testCases := []struct { + args []string + nodeInspectFunc func() (swarm.Node, []byte, error) + nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error + expectedError string + }{ + { + expectedError: "requires at least 1 argument", + }, + { + args: []string{"nodeID"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + }, + expectedError: "error inspecting the node", + }, + { + args: []string{"nodeID"}, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + return fmt.Errorf("error updating the node") + }, + expectedError: "error updating the node", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newPromoteCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: tc.nodeInspectFunc, + nodeUpdateFunc: tc.nodeUpdateFunc, + }, buf)) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNodePromoteNoChange(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newPromoteCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager()), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if node.Role != swarm.NodeRoleManager { + return fmt.Errorf("expected role manager, got %s", node.Role) + } + return nil + }, + }, buf)) + cmd.SetArgs([]string{"nodeID"}) + assert.NilError(t, cmd.Execute()) +} + +func TestNodePromoteMultipleNode(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newPromoteCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if node.Role != swarm.NodeRoleManager { + return fmt.Errorf("expected role manager, got %s", node.Role) + } + return nil + }, + }, buf)) + cmd.SetArgs([]string{"nodeID1", "nodeID2"}) + assert.NilError(t, cmd.Execute()) +} diff --git a/cli/command/node/ps.go b/cli/command/node/ps.go index a034721d24..52ac36646e 100644 --- a/cli/command/node/ps.go +++ b/cli/command/node/ps.go @@ -22,7 +22,7 @@ type psOptions struct { filter opts.FilterOpt } -func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { +func newPsCommand(dockerCli command.Cli) *cobra.Command { opts := psOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -47,7 +47,7 @@ func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runPs(dockerCli *command.DockerCli, opts psOptions) error { +func runPs(dockerCli command.Cli, opts psOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/cli/command/node/ps_test.go b/cli/command/node/ps_test.go new file mode 100644 index 0000000000..1a1022d213 --- /dev/null +++ b/cli/command/node/ps_test.go @@ -0,0 +1,132 @@ +package node + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestNodePsErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + infoFunc func() (types.Info, error) + nodeInspectFunc func() (swarm.Node, []byte, error) + taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error) + taskInspectFunc func(taskID string) (swarm.Task, []byte, error) + expectedError string + }{ + { + infoFunc: func() (types.Info, error) { + return types.Info{}, fmt.Errorf("error asking for node info") + }, + expectedError: "error asking for node info", + }, + { + args: []string{"nodeID"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + }, + expectedError: "error inspecting the node", + }, + { + args: []string{"nodeID"}, + taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { + return []swarm.Task{}, fmt.Errorf("error returning the task list") + }, + expectedError: "error returning the task list", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newPsCommand( + test.NewFakeCli(&fakeClient{ + infoFunc: tc.infoFunc, + nodeInspectFunc: tc.nodeInspectFunc, + taskInspectFunc: tc.taskInspectFunc, + taskListFunc: tc.taskListFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNodePs(t *testing.T) { + testCases := []struct { + name string + args []string + flags map[string]string + infoFunc func() (types.Info, error) + nodeInspectFunc func() (swarm.Node, []byte, error) + taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error) + taskInspectFunc func(taskID string) (swarm.Task, []byte, error) + }{ + { + name: "simple", + args: []string{"nodeID"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(), []byte{}, nil + }, + taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { + return []swarm.Task{ + *Task(WithStatus(Timestamp(time.Now().Add(-2*time.Hour)), PortStatus([]swarm.PortConfig{ + { + TargetPort: 80, + PublishedPort: 80, + Protocol: "tcp", + }, + }))), + }, nil + }, + }, + { + name: "with-errors", + args: []string{"nodeID"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(), []byte{}, nil + }, + taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { + return []swarm.Task{ + *Task(TaskID("taskID1"), ServiceID("failure"), + WithStatus(Timestamp(time.Now().Add(-2*time.Hour)), StatusErr("a task error"))), + *Task(TaskID("taskID2"), ServiceID("failure"), + WithStatus(Timestamp(time.Now().Add(-3*time.Hour)), StatusErr("a task error"))), + *Task(TaskID("taskID3"), ServiceID("failure"), + WithStatus(Timestamp(time.Now().Add(-4*time.Hour)), StatusErr("a task error"))), + }, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newPsCommand( + test.NewFakeCli(&fakeClient{ + infoFunc: tc.infoFunc, + nodeInspectFunc: tc.nodeInspectFunc, + taskInspectFunc: tc.taskInspectFunc, + taskListFunc: tc.taskListFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("node-ps.%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} diff --git a/cli/command/node/remove.go b/cli/command/node/remove.go index 19b4a96631..0e4963aca4 100644 --- a/cli/command/node/remove.go +++ b/cli/command/node/remove.go @@ -16,7 +16,7 @@ type removeOptions struct { force bool } -func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { +func newRemoveCommand(dockerCli command.Cli) *cobra.Command { opts := removeOptions{} cmd := &cobra.Command{ @@ -33,7 +33,7 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runRemove(dockerCli *command.DockerCli, args []string, opts removeOptions) error { +func runRemove(dockerCli command.Cli, args []string, opts removeOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/cli/command/node/remove_test.go b/cli/command/node/remove_test.go new file mode 100644 index 0000000000..54930a276c --- /dev/null +++ b/cli/command/node/remove_test.go @@ -0,0 +1,47 @@ +package node + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestNodeRemoveErrors(t *testing.T) { + testCases := []struct { + args []string + nodeRemoveFunc func() error + expectedError string + }{ + { + expectedError: "requires at least 1 argument", + }, + { + args: []string{"nodeID"}, + nodeRemoveFunc: func() error { + return fmt.Errorf("error removing the node") + }, + expectedError: "error removing the node", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newRemoveCommand( + test.NewFakeCli(&fakeClient{ + nodeRemoveFunc: tc.nodeRemoveFunc, + }, buf)) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNodeRemoveMultiple(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}, buf)) + cmd.SetArgs([]string{"nodeID1", "nodeID2"}) + assert.NilError(t, cmd.Execute()) +} diff --git a/cli/command/node/testdata/node-inspect-pretty.manager-leader.golden b/cli/command/node/testdata/node-inspect-pretty.manager-leader.golden new file mode 100644 index 0000000000..461fc46ea2 --- /dev/null +++ b/cli/command/node/testdata/node-inspect-pretty.manager-leader.golden @@ -0,0 +1,25 @@ +ID: nodeID +Name: defaultNodeName +Hostname: defaultNodeHostname +Joined at: 2009-11-10 23:00:00 +0000 utc +Status: + State: Ready + Availability: Active + Address: 127.0.0.1 +Manager Status: + Address: 127.0.0.1 + Raft Status: Reachable + Leader: Yes +Platform: + Operating System: linux + Architecture: x86_64 +Resources: + CPUs: 0 + Memory: 20 MiB +Plugins: + Network: bridge, overlay + Volume: local +Engine Version: 1.13.0 +Engine Labels: + - engine = label + diff --git a/cli/command/node/testdata/node-inspect-pretty.manager.golden b/cli/command/node/testdata/node-inspect-pretty.manager.golden new file mode 100644 index 0000000000..2c660188d5 --- /dev/null +++ b/cli/command/node/testdata/node-inspect-pretty.manager.golden @@ -0,0 +1,25 @@ +ID: nodeID +Name: defaultNodeName +Hostname: defaultNodeHostname +Joined at: 2009-11-10 23:00:00 +0000 utc +Status: + State: Ready + Availability: Active + Address: 127.0.0.1 +Manager Status: + Address: 127.0.0.1 + Raft Status: Reachable + Leader: No +Platform: + Operating System: linux + Architecture: x86_64 +Resources: + CPUs: 0 + Memory: 20 MiB +Plugins: + Network: bridge, overlay + Volume: local +Engine Version: 1.13.0 +Engine Labels: + - engine = label + diff --git a/cli/command/node/testdata/node-inspect-pretty.simple.golden b/cli/command/node/testdata/node-inspect-pretty.simple.golden new file mode 100644 index 0000000000..e63bc12596 --- /dev/null +++ b/cli/command/node/testdata/node-inspect-pretty.simple.golden @@ -0,0 +1,23 @@ +ID: nodeID +Name: defaultNodeName +Labels: + - lbl1 = value1 +Hostname: defaultNodeHostname +Joined at: 2009-11-10 23:00:00 +0000 utc +Status: + State: Ready + Availability: Active + Address: 127.0.0.1 +Platform: + Operating System: linux + Architecture: x86_64 +Resources: + CPUs: 0 + Memory: 20 MiB +Plugins: + Network: bridge, overlay + Volume: local +Engine Version: 1.13.0 +Engine Labels: + - engine = label + diff --git a/cli/command/node/testdata/node-ps.simple.golden b/cli/command/node/testdata/node-ps.simple.golden new file mode 100644 index 0000000000..f9555d8792 --- /dev/null +++ b/cli/command/node/testdata/node-ps.simple.golden @@ -0,0 +1,2 @@ +ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS +taskID rl02d5gwz6chzu7il5fhtb8be.1 myimage:mytag defaultNodeName Ready Ready 2 hours ago *:80->80/tcp diff --git a/cli/command/node/testdata/node-ps.with-errors.golden b/cli/command/node/testdata/node-ps.with-errors.golden new file mode 100644 index 0000000000..273b30fa11 --- /dev/null +++ b/cli/command/node/testdata/node-ps.with-errors.golden @@ -0,0 +1,4 @@ +ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS +taskID1 failure.1 myimage:mytag defaultNodeName Ready Ready 2 hours ago "a task error" +taskID2 \_ failure.1 myimage:mytag defaultNodeName Ready Ready 3 hours ago "a task error" +taskID3 \_ failure.1 myimage:mytag defaultNodeName Ready Ready 4 hours ago "a task error" diff --git a/cli/command/node/update.go b/cli/command/node/update.go index 65339e138b..6ca2a7c1e3 100644 --- a/cli/command/node/update.go +++ b/cli/command/node/update.go @@ -18,7 +18,7 @@ var ( errNoRoleChange = errors.New("role was already set to the requested value") ) -func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { +func newUpdateCommand(dockerCli command.Cli) *cobra.Command { nodeOpts := newNodeOptions() cmd := &cobra.Command{ @@ -39,14 +39,14 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, nodeID string) error { +func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, nodeID string) error { success := func(_ string) { fmt.Fprintln(dockerCli.Out(), nodeID) } return updateNodes(dockerCli, []string{nodeID}, mergeNodeUpdate(flags), success) } -func updateNodes(dockerCli *command.DockerCli, nodes []string, mergeNode func(node *swarm.Node) error, success func(nodeID string)) error { +func updateNodes(dockerCli command.Cli, nodes []string, mergeNode func(node *swarm.Node) error, success func(nodeID string)) error { client := dockerCli.Client() ctx := context.Background() diff --git a/cli/command/node/update_test.go b/cli/command/node/update_test.go new file mode 100644 index 0000000000..439ba94436 --- /dev/null +++ b/cli/command/node/update_test.go @@ -0,0 +1,172 @@ +package node + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestNodeUpdateErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + nodeInspectFunc func() (swarm.Node, []byte, error) + nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error + expectedError string + }{ + { + expectedError: "requires exactly 1 argument", + }, + { + args: []string{"node1", "node2"}, + expectedError: "requires exactly 1 argument", + }, + { + args: []string{"nodeID"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + }, + expectedError: "error inspecting the node", + }, + { + args: []string{"nodeID"}, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + return fmt.Errorf("error updating the node") + }, + expectedError: "error updating the node", + }, + { + args: []string{"nodeID"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(NodeLabels(map[string]string{ + "key": "value", + })), []byte{}, nil + }, + flags: map[string]string{ + "label-rm": "notpresent", + }, + expectedError: "key notpresent doesn't exist in node's labels", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newUpdateCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: tc.nodeInspectFunc, + nodeUpdateFunc: tc.nodeUpdateFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNodeUpdate(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + nodeInspectFunc func() (swarm.Node, []byte, error) + nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error + }{ + { + args: []string{"nodeID"}, + flags: map[string]string{ + "role": "manager", + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if node.Role != swarm.NodeRoleManager { + return fmt.Errorf("expected role manager, got %s", node.Role) + } + return nil + }, + }, + { + args: []string{"nodeID"}, + flags: map[string]string{ + "availability": "drain", + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if node.Availability != swarm.NodeAvailabilityDrain { + return fmt.Errorf("expected drain availability, got %s", node.Availability) + } + return nil + }, + }, + { + args: []string{"nodeID"}, + flags: map[string]string{ + "label-add": "lbl", + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if _, present := node.Annotations.Labels["lbl"]; !present { + return fmt.Errorf("expected 'lbl' label, got %v", node.Annotations.Labels) + } + return nil + }, + }, + { + args: []string{"nodeID"}, + flags: map[string]string{ + "label-add": "key=value", + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if value, present := node.Annotations.Labels["key"]; !present || value != "value" { + return fmt.Errorf("expected 'key' label to be 'value', got %v", node.Annotations.Labels) + } + return nil + }, + }, + { + args: []string{"nodeID"}, + flags: map[string]string{ + "label-rm": "key", + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(NodeLabels(map[string]string{ + "key": "value", + })), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if len(node.Annotations.Labels) > 0 { + return fmt.Errorf("expected no labels, got %v", node.Annotations.Labels) + } + return nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newUpdateCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: tc.nodeInspectFunc, + nodeUpdateFunc: tc.nodeUpdateFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + assert.NilError(t, cmd.Execute()) + } +} diff --git a/cli/command/swarm/client_test.go b/cli/command/swarm/client_test.go new file mode 100644 index 0000000000..1d42b9499c --- /dev/null +++ b/cli/command/swarm/client_test.go @@ -0,0 +1,84 @@ +package swarm + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" + "golang.org/x/net/context" +) + +type fakeClient struct { + client.Client + infoFunc func() (types.Info, error) + swarmInitFunc func() (string, error) + swarmInspectFunc func() (swarm.Swarm, error) + nodeInspectFunc func() (swarm.Node, []byte, error) + swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) + swarmJoinFunc func() error + swarmLeaveFunc func() error + swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error + swarmUnlockFunc func(req swarm.UnlockRequest) error +} + +func (cli *fakeClient) Info(ctx context.Context) (types.Info, error) { + if cli.infoFunc != nil { + return cli.infoFunc() + } + return types.Info{}, nil +} + +func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, ref string) (swarm.Node, []byte, error) { + if cli.nodeInspectFunc != nil { + return cli.nodeInspectFunc() + } + return swarm.Node{}, []byte{}, nil +} + +func (cli *fakeClient) SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error) { + if cli.swarmInitFunc != nil { + return cli.swarmInitFunc() + } + return "", nil +} + +func (cli *fakeClient) SwarmInspect(ctx context.Context) (swarm.Swarm, error) { + if cli.swarmInspectFunc != nil { + return cli.swarmInspectFunc() + } + return swarm.Swarm{}, nil +} + +func (cli *fakeClient) SwarmGetUnlockKey(ctx context.Context) (types.SwarmUnlockKeyResponse, error) { + if cli.swarmGetUnlockKeyFunc != nil { + return cli.swarmGetUnlockKeyFunc() + } + return types.SwarmUnlockKeyResponse{}, nil +} + +func (cli *fakeClient) SwarmJoin(ctx context.Context, req swarm.JoinRequest) error { + if cli.swarmJoinFunc != nil { + return cli.swarmJoinFunc() + } + return nil +} + +func (cli *fakeClient) SwarmLeave(ctx context.Context, force bool) error { + if cli.swarmLeaveFunc != nil { + return cli.swarmLeaveFunc() + } + return nil +} + +func (cli *fakeClient) SwarmUpdate(ctx context.Context, version swarm.Version, swarm swarm.Spec, flags swarm.UpdateFlags) error { + if cli.swarmUpdateFunc != nil { + return cli.swarmUpdateFunc(swarm, flags) + } + return nil +} + +func (cli *fakeClient) SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error { + if cli.swarmUnlockFunc != nil { + return cli.swarmUnlockFunc(req) + } + return nil +} diff --git a/cli/command/swarm/init.go b/cli/command/swarm/init.go index 2550feeb47..e038ac62a5 100644 --- a/cli/command/swarm/init.go +++ b/cli/command/swarm/init.go @@ -22,7 +22,7 @@ type initOptions struct { forceNewCluster bool } -func newInitCommand(dockerCli *command.DockerCli) *cobra.Command { +func newInitCommand(dockerCli command.Cli) *cobra.Command { opts := initOptions{ listenAddr: NewListenAddrOption(), } @@ -45,7 +45,7 @@ func newInitCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runInit(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts initOptions) error { +func runInit(dockerCli command.Cli, flags *pflag.FlagSet, opts initOptions) error { client := dockerCli.Client() ctx := context.Background() @@ -67,7 +67,7 @@ func runInit(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts initOption fmt.Fprintf(dockerCli.Out(), "Swarm initialized: current node (%s) is now a manager.\n\n", nodeID) - if err := printJoinCommand(ctx, dockerCli, nodeID, true, false); err != nil { + if err := printJoinCommand(ctx, dockerCli, nodeID, false, true); err != nil { return err } diff --git a/cli/command/swarm/init_test.go b/cli/command/swarm/init_test.go new file mode 100644 index 0000000000..13de1cd550 --- /dev/null +++ b/cli/command/swarm/init_test.go @@ -0,0 +1,129 @@ +package swarm + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestSwarmInitErrorOnAPIFailure(t *testing.T) { + testCases := []struct { + name string + flags map[string]string + swarmInitFunc func() (string, error) + swarmInspectFunc func() (swarm.Swarm, error) + swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) + nodeInspectFunc func() (swarm.Node, []byte, error) + expectedError string + }{ + { + name: "init-failed", + swarmInitFunc: func() (string, error) { + return "", fmt.Errorf("error initializing the swarm") + }, + expectedError: "error initializing the swarm", + }, + { + name: "init-faild-with-ip-choice", + swarmInitFunc: func() (string, error) { + return "", fmt.Errorf("could not choose an IP address to advertise") + }, + expectedError: "could not choose an IP address to advertise - specify one with --advertise-addr", + }, + { + name: "swarm-inspect-after-init-failed", + swarmInspectFunc: func() (swarm.Swarm, error) { + return swarm.Swarm{}, fmt.Errorf("error inspecting the swarm") + }, + expectedError: "error inspecting the swarm", + }, + { + name: "node-inspect-after-init-failed", + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + }, + expectedError: "error inspecting the node", + }, + { + name: "swarm-get-unlock-key-after-init-failed", + flags: map[string]string{ + flagAutolock: "true", + }, + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{}, fmt.Errorf("error getting swarm unlock key") + }, + expectedError: "could not fetch unlock key: error getting swarm unlock key", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newInitCommand( + test.NewFakeCli(&fakeClient{ + swarmInitFunc: tc.swarmInitFunc, + swarmInspectFunc: tc.swarmInspectFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + nodeInspectFunc: tc.nodeInspectFunc, + }, buf)) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSwarmInit(t *testing.T) { + testCases := []struct { + name string + flags map[string]string + swarmInitFunc func() (string, error) + swarmInspectFunc func() (swarm.Swarm, error) + swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) + nodeInspectFunc func() (swarm.Node, []byte, error) + }{ + { + name: "init", + swarmInitFunc: func() (string, error) { + return "nodeID", nil + }, + }, + { + name: "init-autolock", + flags: map[string]string{ + flagAutolock: "true", + }, + swarmInitFunc: func() (string, error) { + return "nodeID", nil + }, + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{ + UnlockKey: "unlock-key", + }, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newInitCommand( + test.NewFakeCli(&fakeClient{ + swarmInitFunc: tc.swarmInitFunc, + swarmInspectFunc: tc.swarmInspectFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + nodeInspectFunc: tc.nodeInspectFunc, + }, buf)) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("init-%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} diff --git a/cli/command/swarm/join.go b/cli/command/swarm/join.go index 004313b4c6..3ea1462df4 100644 --- a/cli/command/swarm/join.go +++ b/cli/command/swarm/join.go @@ -18,7 +18,7 @@ type joinOptions struct { token string } -func newJoinCommand(dockerCli *command.DockerCli) *cobra.Command { +func newJoinCommand(dockerCli command.Cli) *cobra.Command { opts := joinOptions{ listenAddr: NewListenAddrOption(), } @@ -40,7 +40,7 @@ func newJoinCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runJoin(dockerCli *command.DockerCli, opts joinOptions) error { +func runJoin(dockerCli command.Cli, opts joinOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/cli/command/swarm/join_test.go b/cli/command/swarm/join_test.go new file mode 100644 index 0000000000..66dd6d66b6 --- /dev/null +++ b/cli/command/swarm/join_test.go @@ -0,0 +1,102 @@ +package swarm + +import ( + "bytes" + "fmt" + "io/ioutil" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestSwarmJoinErrors(t *testing.T) { + testCases := []struct { + name string + args []string + swarmJoinFunc func() error + infoFunc func() (types.Info, error) + expectedError string + }{ + { + name: "not-enough-args", + expectedError: "requires exactly 1 argument", + }, + { + name: "too-many-args", + args: []string{"remote1", "remote2"}, + expectedError: "requires exactly 1 argument", + }, + { + name: "join-failed", + args: []string{"remote"}, + swarmJoinFunc: func() error { + return fmt.Errorf("error joining the swarm") + }, + expectedError: "error joining the swarm", + }, + { + name: "join-failed-on-init", + args: []string{"remote"}, + infoFunc: func() (types.Info, error) { + return types.Info{}, fmt.Errorf("error asking for node info") + }, + expectedError: "error asking for node info", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newJoinCommand( + test.NewFakeCli(&fakeClient{ + swarmJoinFunc: tc.swarmJoinFunc, + infoFunc: tc.infoFunc, + }, buf)) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSwarmJoin(t *testing.T) { + testCases := []struct { + name string + infoFunc func() (types.Info, error) + expected string + }{ + { + name: "join-as-manager", + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + ControlAvailable: true, + }, + }, nil + }, + expected: "This node joined a swarm as a manager.", + }, + { + name: "join-as-worker", + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + ControlAvailable: false, + }, + }, nil + }, + expected: "This node joined a swarm as a worker.", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newJoinCommand( + test.NewFakeCli(&fakeClient{ + infoFunc: tc.infoFunc, + }, buf)) + cmd.SetArgs([]string{"remote"}) + assert.NilError(t, cmd.Execute()) + assert.Equal(t, strings.TrimSpace(buf.String()), tc.expected) + } +} diff --git a/cli/command/swarm/join_token.go b/cli/command/swarm/join_token.go index d800b769ba..5c84c7a310 100644 --- a/cli/command/swarm/join_token.go +++ b/cli/command/swarm/join_token.go @@ -18,7 +18,7 @@ type joinTokenOptions struct { quiet bool } -func newJoinTokenCommand(dockerCli *command.DockerCli) *cobra.Command { +func newJoinTokenCommand(dockerCli command.Cli) *cobra.Command { opts := joinTokenOptions{} cmd := &cobra.Command{ @@ -38,7 +38,7 @@ func newJoinTokenCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runJoinToken(dockerCli *command.DockerCli, opts joinTokenOptions) error { +func runJoinToken(dockerCli command.Cli, opts joinTokenOptions) error { worker := opts.role == "worker" manager := opts.role == "manager" @@ -94,7 +94,7 @@ func runJoinToken(dockerCli *command.DockerCli, opts joinTokenOptions) error { return printJoinCommand(ctx, dockerCli, info.Swarm.NodeID, worker, manager) } -func printJoinCommand(ctx context.Context, dockerCli *command.DockerCli, nodeID string, worker bool, manager bool) error { +func printJoinCommand(ctx context.Context, dockerCli command.Cli, nodeID string, worker bool, manager bool) error { client := dockerCli.Client() node, _, err := client.NodeInspectWithRaw(ctx, nodeID) diff --git a/cli/command/swarm/join_token_test.go b/cli/command/swarm/join_token_test.go new file mode 100644 index 0000000000..6244016419 --- /dev/null +++ b/cli/command/swarm/join_token_test.go @@ -0,0 +1,215 @@ +package swarm + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestSwarmJoinTokenErrors(t *testing.T) { + testCases := []struct { + name string + args []string + flags map[string]string + infoFunc func() (types.Info, error) + swarmInspectFunc func() (swarm.Swarm, error) + swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error + nodeInspectFunc func() (swarm.Node, []byte, error) + expectedError string + }{ + { + name: "not-enough-args", + expectedError: "requires exactly 1 argument", + }, + { + name: "too-many-args", + args: []string{"worker", "manager"}, + expectedError: "requires exactly 1 argument", + }, + { + name: "invalid-args", + args: []string{"foo"}, + expectedError: "unknown role foo", + }, + { + name: "swarm-inspect-failed", + args: []string{"worker"}, + swarmInspectFunc: func() (swarm.Swarm, error) { + return swarm.Swarm{}, fmt.Errorf("error inspecting the swarm") + }, + expectedError: "error inspecting the swarm", + }, + { + name: "swarm-inspect-rotate-failed", + args: []string{"worker"}, + flags: map[string]string{ + flagRotate: "true", + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return swarm.Swarm{}, fmt.Errorf("error inspecting the swarm") + }, + expectedError: "error inspecting the swarm", + }, + { + name: "swarm-update-failed", + args: []string{"worker"}, + flags: map[string]string{ + flagRotate: "true", + }, + swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { + return fmt.Errorf("error updating the swarm") + }, + expectedError: "error updating the swarm", + }, + { + name: "node-inspect-failed", + args: []string{"worker"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting node") + }, + expectedError: "error inspecting node", + }, + { + name: "info-failed", + args: []string{"worker"}, + infoFunc: func() (types.Info, error) { + return types.Info{}, fmt.Errorf("error asking for node info") + }, + expectedError: "error asking for node info", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newJoinTokenCommand( + test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + swarmUpdateFunc: tc.swarmUpdateFunc, + infoFunc: tc.infoFunc, + nodeInspectFunc: tc.nodeInspectFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSwarmJoinToken(t *testing.T) { + testCases := []struct { + name string + args []string + flags map[string]string + infoFunc func() (types.Info, error) + swarmInspectFunc func() (swarm.Swarm, error) + nodeInspectFunc func() (swarm.Node, []byte, error) + }{ + { + name: "worker", + args: []string{"worker"}, + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + NodeID: "nodeID", + }, + }, nil + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager()), []byte{}, nil + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(), nil + }, + }, + { + name: "manager", + args: []string{"manager"}, + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + NodeID: "nodeID", + }, + }, nil + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager()), []byte{}, nil + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(), nil + }, + }, + { + name: "manager-rotate", + args: []string{"manager"}, + flags: map[string]string{ + flagRotate: "true", + }, + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + NodeID: "nodeID", + }, + }, nil + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager()), []byte{}, nil + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(), nil + }, + }, + { + name: "worker-quiet", + args: []string{"worker"}, + flags: map[string]string{ + flagQuiet: "true", + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager()), []byte{}, nil + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(), nil + }, + }, + { + name: "manager-quiet", + args: []string{"manager"}, + flags: map[string]string{ + flagQuiet: "true", + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager()), []byte{}, nil + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(), nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newJoinTokenCommand( + test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + infoFunc: tc.infoFunc, + nodeInspectFunc: tc.nodeInspectFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("jointoken-%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} diff --git a/cli/command/swarm/leave.go b/cli/command/swarm/leave.go index e2cfa0a045..128ed46d8a 100644 --- a/cli/command/swarm/leave.go +++ b/cli/command/swarm/leave.go @@ -14,7 +14,7 @@ type leaveOptions struct { force bool } -func newLeaveCommand(dockerCli *command.DockerCli) *cobra.Command { +func newLeaveCommand(dockerCli command.Cli) *cobra.Command { opts := leaveOptions{} cmd := &cobra.Command{ @@ -31,7 +31,7 @@ func newLeaveCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runLeave(dockerCli *command.DockerCli, opts leaveOptions) error { +func runLeave(dockerCli command.Cli, opts leaveOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/cli/command/swarm/leave_test.go b/cli/command/swarm/leave_test.go new file mode 100644 index 0000000000..09b41b2511 --- /dev/null +++ b/cli/command/swarm/leave_test.go @@ -0,0 +1,52 @@ +package swarm + +import ( + "bytes" + "fmt" + "io/ioutil" + "strings" + "testing" + + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestSwarmLeaveErrors(t *testing.T) { + testCases := []struct { + name string + args []string + swarmLeaveFunc func() error + expectedError string + }{ + { + name: "too-many-args", + args: []string{"foo"}, + expectedError: "accepts no argument(s)", + }, + { + name: "leave-failed", + swarmLeaveFunc: func() error { + return fmt.Errorf("error leaving the swarm") + }, + expectedError: "error leaving the swarm", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newLeaveCommand( + test.NewFakeCli(&fakeClient{ + swarmLeaveFunc: tc.swarmLeaveFunc, + }, buf)) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSwarmLeave(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newLeaveCommand( + test.NewFakeCli(&fakeClient{}, buf)) + assert.NilError(t, cmd.Execute()) + assert.Equal(t, strings.TrimSpace(buf.String()), "Node left the swarm.") +} diff --git a/cli/command/swarm/opts_test.go b/cli/command/swarm/opts_test.go index 568dc87302..9a97e8bd2c 100644 --- a/cli/command/swarm/opts_test.go +++ b/cli/command/swarm/opts_test.go @@ -35,3 +35,76 @@ func TestNodeAddrOptionSetInvalidFormat(t *testing.T) { opt := NewListenAddrOption() assert.Error(t, opt.Set("http://localhost:4545"), "Invalid") } + +func TestExternalCAOptionErrors(t *testing.T) { + testCases := []struct { + externalCA string + expectedError string + }{ + { + externalCA: "", + expectedError: "EOF", + }, + { + externalCA: "anything", + expectedError: "invalid field 'anything' must be a key=value pair", + }, + { + externalCA: "foo=bar", + expectedError: "the external-ca option needs a protocol= parameter", + }, + { + externalCA: "protocol=baz", + expectedError: "unrecognized external CA protocol baz", + }, + { + externalCA: "protocol=cfssl", + expectedError: "the external-ca option needs a url= parameter", + }, + } + for _, tc := range testCases { + opt := &ExternalCAOption{} + assert.Error(t, opt.Set(tc.externalCA), tc.expectedError) + } +} + +func TestExternalCAOption(t *testing.T) { + testCases := []struct { + externalCA string + expected string + }{ + { + externalCA: "protocol=cfssl,url=anything", + expected: "cfssl: anything", + }, + { + externalCA: "protocol=CFSSL,url=anything", + expected: "cfssl: anything", + }, + { + externalCA: "protocol=Cfssl,url=https://example.com", + expected: "cfssl: https://example.com", + }, + { + externalCA: "protocol=Cfssl,url=https://example.com,foo=bar", + expected: "cfssl: https://example.com", + }, + { + externalCA: "protocol=Cfssl,url=https://example.com,foo=bar,foo=baz", + expected: "cfssl: https://example.com", + }, + } + for _, tc := range testCases { + opt := &ExternalCAOption{} + assert.NilError(t, opt.Set(tc.externalCA)) + assert.Equal(t, opt.String(), tc.expected) + } +} + +func TestExternalCAOptionMultiple(t *testing.T) { + opt := &ExternalCAOption{} + assert.NilError(t, opt.Set("protocol=cfssl,url=https://example.com")) + assert.NilError(t, opt.Set("protocol=CFSSL,url=anything")) + assert.Equal(t, len(opt.Value()), 2) + assert.Equal(t, opt.String(), "cfssl: https://example.com, cfssl: anything") +} diff --git a/cli/command/swarm/testdata/init-init-autolock.golden b/cli/command/swarm/testdata/init-init-autolock.golden new file mode 100644 index 0000000000..cdd3c666b6 --- /dev/null +++ b/cli/command/swarm/testdata/init-init-autolock.golden @@ -0,0 +1,11 @@ +Swarm initialized: current node (nodeID) is now a manager. + +To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions. + +To unlock a swarm manager after it restarts, run the `docker swarm unlock` +command and provide the following key: + + unlock-key + +Please remember to store this key in a password manager, since without it you +will not be able to restart the manager. diff --git a/cli/command/swarm/testdata/init-init.golden b/cli/command/swarm/testdata/init-init.golden new file mode 100644 index 0000000000..6e82be010e --- /dev/null +++ b/cli/command/swarm/testdata/init-init.golden @@ -0,0 +1,4 @@ +Swarm initialized: current node (nodeID) is now a manager. + +To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions. + diff --git a/cli/command/swarm/testdata/jointoken-manager-quiet.golden b/cli/command/swarm/testdata/jointoken-manager-quiet.golden new file mode 100644 index 0000000000..0c7cfc6088 --- /dev/null +++ b/cli/command/swarm/testdata/jointoken-manager-quiet.golden @@ -0,0 +1 @@ +manager-join-token diff --git a/cli/command/swarm/testdata/jointoken-manager-rotate.golden b/cli/command/swarm/testdata/jointoken-manager-rotate.golden new file mode 100644 index 0000000000..7ee455bec8 --- /dev/null +++ b/cli/command/swarm/testdata/jointoken-manager-rotate.golden @@ -0,0 +1,8 @@ +Successfully rotated manager join token. + +To add a manager to this swarm, run the following command: + + docker swarm join \ + --token manager-join-token \ + 127.0.0.1 + diff --git a/cli/command/swarm/testdata/jointoken-manager.golden b/cli/command/swarm/testdata/jointoken-manager.golden new file mode 100644 index 0000000000..d56527aa55 --- /dev/null +++ b/cli/command/swarm/testdata/jointoken-manager.golden @@ -0,0 +1,6 @@ +To add a manager to this swarm, run the following command: + + docker swarm join \ + --token manager-join-token \ + 127.0.0.1 + diff --git a/cli/command/swarm/testdata/jointoken-worker-quiet.golden b/cli/command/swarm/testdata/jointoken-worker-quiet.golden new file mode 100644 index 0000000000..b445e191e5 --- /dev/null +++ b/cli/command/swarm/testdata/jointoken-worker-quiet.golden @@ -0,0 +1 @@ +worker-join-token diff --git a/cli/command/swarm/testdata/jointoken-worker.golden b/cli/command/swarm/testdata/jointoken-worker.golden new file mode 100644 index 0000000000..5d44f3daee --- /dev/null +++ b/cli/command/swarm/testdata/jointoken-worker.golden @@ -0,0 +1,6 @@ +To add a worker to this swarm, run the following command: + + docker swarm join \ + --token worker-join-token \ + 127.0.0.1 + diff --git a/cli/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden b/cli/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden new file mode 100644 index 0000000000..ed53505e25 --- /dev/null +++ b/cli/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden @@ -0,0 +1 @@ +unlock-key diff --git a/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden b/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden new file mode 100644 index 0000000000..ed53505e25 --- /dev/null +++ b/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden @@ -0,0 +1 @@ +unlock-key diff --git a/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden b/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden new file mode 100644 index 0000000000..89152b8643 --- /dev/null +++ b/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden @@ -0,0 +1,9 @@ +Successfully rotated manager unlock key. + +To unlock a swarm manager after it restarts, run the `docker swarm unlock` +command and provide the following key: + + unlock-key + +Please remember to store this key in a password manager, since without it you +will not be able to restart the manager. diff --git a/cli/command/swarm/testdata/unlockkeys-unlock-key.golden b/cli/command/swarm/testdata/unlockkeys-unlock-key.golden new file mode 100644 index 0000000000..8316df478c --- /dev/null +++ b/cli/command/swarm/testdata/unlockkeys-unlock-key.golden @@ -0,0 +1,7 @@ +To unlock a swarm manager after it restarts, run the `docker swarm unlock` +command and provide the following key: + + unlock-key + +Please remember to store this key in a password manager, since without it you +will not be able to restart the manager. diff --git a/cli/command/swarm/testdata/update-all-flags-quiet.golden b/cli/command/swarm/testdata/update-all-flags-quiet.golden new file mode 100644 index 0000000000..3d195a2586 --- /dev/null +++ b/cli/command/swarm/testdata/update-all-flags-quiet.golden @@ -0,0 +1 @@ +Swarm updated. diff --git a/cli/command/swarm/testdata/update-autolock-unlock-key.golden b/cli/command/swarm/testdata/update-autolock-unlock-key.golden new file mode 100644 index 0000000000..a077b9e167 --- /dev/null +++ b/cli/command/swarm/testdata/update-autolock-unlock-key.golden @@ -0,0 +1,8 @@ +Swarm updated. +To unlock a swarm manager after it restarts, run the `docker swarm unlock` +command and provide the following key: + + unlock-key + +Please remember to store this key in a password manager, since without it you +will not be able to restart the manager. diff --git a/cli/command/swarm/testdata/update-noargs.golden b/cli/command/swarm/testdata/update-noargs.golden new file mode 100644 index 0000000000..381c0ccf1f --- /dev/null +++ b/cli/command/swarm/testdata/update-noargs.golden @@ -0,0 +1,13 @@ +Update the swarm + +Usage: + update [OPTIONS] [flags] + +Flags: + --autolock Change manager autolocking setting (true|false) + --cert-expiry duration Validity period for node certificates (ns|us|ms|s|m|h) (default 2160h0m0s) + --dispatcher-heartbeat duration Dispatcher heartbeat period (ns|us|ms|s|m|h) (default 5s) + --external-ca external-ca Specifications of one or more certificate signing endpoints + --max-snapshots uint Number of additional Raft snapshots to retain + --snapshot-interval uint Number of log entries between Raft snapshots (default 10000) + --task-history-limit int Task history retention limit (default 5) diff --git a/cli/command/swarm/unlock.go b/cli/command/swarm/unlock.go index aa752e2148..45dd6e79e3 100644 --- a/cli/command/swarm/unlock.go +++ b/cli/command/swarm/unlock.go @@ -18,7 +18,7 @@ import ( type unlockOptions struct{} -func newUnlockCommand(dockerCli *command.DockerCli) *cobra.Command { +func newUnlockCommand(dockerCli command.Cli) *cobra.Command { opts := unlockOptions{} cmd := &cobra.Command{ @@ -33,7 +33,7 @@ func newUnlockCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runUnlock(dockerCli *command.DockerCli, opts unlockOptions) error { +func runUnlock(dockerCli command.Cli, opts unlockOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/cli/command/swarm/unlock_key.go b/cli/command/swarm/unlock_key.go index e571e6645f..77c97d88ea 100644 --- a/cli/command/swarm/unlock_key.go +++ b/cli/command/swarm/unlock_key.go @@ -3,12 +3,11 @@ package swarm import ( "fmt" - "github.com/spf13/cobra" - "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/pkg/errors" + "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -17,7 +16,7 @@ type unlockKeyOptions struct { quiet bool } -func newUnlockKeyCommand(dockerCli *command.DockerCli) *cobra.Command { +func newUnlockKeyCommand(dockerCli command.Cli) *cobra.Command { opts := unlockKeyOptions{} cmd := &cobra.Command{ @@ -36,7 +35,7 @@ func newUnlockKeyCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runUnlockKey(dockerCli *command.DockerCli, opts unlockKeyOptions) error { +func runUnlockKey(dockerCli command.Cli, opts unlockKeyOptions) error { client := dockerCli.Client() ctx := context.Background() @@ -79,7 +78,7 @@ func runUnlockKey(dockerCli *command.DockerCli, opts unlockKeyOptions) error { return nil } -func printUnlockCommand(ctx context.Context, dockerCli *command.DockerCli, unlockKey string) { +func printUnlockCommand(ctx context.Context, dockerCli command.Cli, unlockKey string) { if len(unlockKey) > 0 { fmt.Fprintf(dockerCli.Out(), "To unlock a swarm manager after it restarts, run the `docker swarm unlock`\ncommand and provide the following key:\n\n %s\n\nPlease remember to store this key in a password manager, since without it you\nwill not be able to restart the manager.\n", unlockKey) } diff --git a/cli/command/swarm/unlock_key_test.go b/cli/command/swarm/unlock_key_test.go new file mode 100644 index 0000000000..17a07d3fb1 --- /dev/null +++ b/cli/command/swarm/unlock_key_test.go @@ -0,0 +1,175 @@ +package swarm + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestSwarmUnlockKeyErrors(t *testing.T) { + testCases := []struct { + name string + args []string + flags map[string]string + swarmInspectFunc func() (swarm.Swarm, error) + swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error + swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) + expectedError string + }{ + { + name: "too-many-args", + args: []string{"foo"}, + expectedError: "accepts no argument(s)", + }, + { + name: "swarm-inspect-rotate-failed", + flags: map[string]string{ + flagRotate: "true", + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return swarm.Swarm{}, fmt.Errorf("error inspecting the swarm") + }, + expectedError: "error inspecting the swarm", + }, + { + name: "swarm-rotate-no-autolock-failed", + flags: map[string]string{ + flagRotate: "true", + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(), nil + }, + expectedError: "cannot rotate because autolock is not turned on", + }, + { + name: "swarm-update-failed", + flags: map[string]string{ + flagRotate: "true", + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(Autolock()), nil + }, + swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { + return fmt.Errorf("error updating the swarm") + }, + expectedError: "error updating the swarm", + }, + { + name: "swarm-get-unlock-key-failed", + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{}, fmt.Errorf("error getting unlock key") + }, + expectedError: "error getting unlock key", + }, + { + name: "swarm-no-unlock-key-failed", + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{ + UnlockKey: "", + }, nil + }, + expectedError: "no unlock key is set", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newUnlockKeyCommand( + test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + swarmUpdateFunc: tc.swarmUpdateFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSwarmUnlockKey(t *testing.T) { + testCases := []struct { + name string + args []string + flags map[string]string + swarmInspectFunc func() (swarm.Swarm, error) + swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error + swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) + }{ + { + name: "unlock-key", + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{ + UnlockKey: "unlock-key", + }, nil + }, + }, + { + name: "unlock-key-quiet", + flags: map[string]string{ + flagQuiet: "true", + }, + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{ + UnlockKey: "unlock-key", + }, nil + }, + }, + { + name: "unlock-key-rotate", + flags: map[string]string{ + flagRotate: "true", + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(Autolock()), nil + }, + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{ + UnlockKey: "unlock-key", + }, nil + }, + }, + { + name: "unlock-key-rotate-quiet", + flags: map[string]string{ + flagQuiet: "true", + flagRotate: "true", + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(Autolock()), nil + }, + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{ + UnlockKey: "unlock-key", + }, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newUnlockKeyCommand( + test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + swarmUpdateFunc: tc.swarmUpdateFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("unlockkeys-%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} diff --git a/cli/command/swarm/unlock_test.go b/cli/command/swarm/unlock_test.go new file mode 100644 index 0000000000..abf858a289 --- /dev/null +++ b/cli/command/swarm/unlock_test.go @@ -0,0 +1,101 @@ +package swarm + +import ( + "bytes" + "fmt" + "io/ioutil" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestSwarmUnlockErrors(t *testing.T) { + testCases := []struct { + name string + args []string + input string + swarmUnlockFunc func(req swarm.UnlockRequest) error + infoFunc func() (types.Info, error) + expectedError string + }{ + { + name: "too-many-args", + args: []string{"foo"}, + expectedError: "accepts no argument(s)", + }, + { + name: "is-not-part-of-a-swarm", + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + LocalNodeState: swarm.LocalNodeStateInactive, + }, + }, nil + }, + expectedError: "This node is not part of a swarm", + }, + { + name: "is-not-locked", + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + LocalNodeState: swarm.LocalNodeStateActive, + }, + }, nil + }, + expectedError: "Error: swarm is not locked", + }, + { + name: "unlockrequest-failed", + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + LocalNodeState: swarm.LocalNodeStateLocked, + }, + }, nil + }, + swarmUnlockFunc: func(req swarm.UnlockRequest) error { + return fmt.Errorf("error unlocking the swarm") + }, + expectedError: "error unlocking the swarm", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newUnlockCommand( + test.NewFakeCli(&fakeClient{ + infoFunc: tc.infoFunc, + swarmUnlockFunc: tc.swarmUnlockFunc, + }, buf)) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSwarmUnlock(t *testing.T) { + input := "unlockKey" + buf := new(bytes.Buffer) + dockerCli := test.NewFakeCli(&fakeClient{ + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + LocalNodeState: swarm.LocalNodeStateLocked, + }, + }, nil + }, + swarmUnlockFunc: func(req swarm.UnlockRequest) error { + if req.UnlockKey != input { + return fmt.Errorf("Invalid unlock key") + } + return nil + }, + }, buf) + dockerCli.SetIn(ioutil.NopCloser(strings.NewReader(input))) + cmd := newUnlockCommand(dockerCli) + assert.NilError(t, cmd.Execute()) +} diff --git a/cli/command/swarm/update.go b/cli/command/swarm/update.go index dbbd268725..1ccd268e74 100644 --- a/cli/command/swarm/update.go +++ b/cli/command/swarm/update.go @@ -13,7 +13,7 @@ import ( "github.com/spf13/pflag" ) -func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { +func newUpdateCommand(dockerCli command.Cli) *cobra.Command { opts := swarmOptions{} cmd := &cobra.Command{ @@ -36,24 +36,24 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts swarmOptions) error { +func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, opts swarmOptions) error { client := dockerCli.Client() ctx := context.Background() var updateFlags swarm.UpdateFlags - swarm, err := client.SwarmInspect(ctx) + swarmInspect, err := client.SwarmInspect(ctx) if err != nil { return err } - prevAutoLock := swarm.Spec.EncryptionConfig.AutoLockManagers + prevAutoLock := swarmInspect.Spec.EncryptionConfig.AutoLockManagers - opts.mergeSwarmSpec(&swarm.Spec, flags) + opts.mergeSwarmSpec(&swarmInspect.Spec, flags) - curAutoLock := swarm.Spec.EncryptionConfig.AutoLockManagers + curAutoLock := swarmInspect.Spec.EncryptionConfig.AutoLockManagers - err = client.SwarmUpdate(ctx, swarm.Version, swarm.Spec, updateFlags) + err = client.SwarmUpdate(ctx, swarmInspect.Version, swarmInspect.Spec, updateFlags) if err != nil { return err } diff --git a/cli/command/swarm/update_test.go b/cli/command/swarm/update_test.go new file mode 100644 index 0000000000..c8a2860a00 --- /dev/null +++ b/cli/command/swarm/update_test.go @@ -0,0 +1,182 @@ +package swarm + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestSwarmUpdateErrors(t *testing.T) { + testCases := []struct { + name string + args []string + flags map[string]string + swarmInspectFunc func() (swarm.Swarm, error) + swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error + swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) + expectedError string + }{ + { + name: "too-many-args", + args: []string{"foo"}, + expectedError: "accepts no argument(s)", + }, + { + name: "swarm-inspect-error", + flags: map[string]string{ + flagTaskHistoryLimit: "10", + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return swarm.Swarm{}, fmt.Errorf("error inspecting the swarm") + }, + expectedError: "error inspecting the swarm", + }, + { + name: "swarm-update-error", + flags: map[string]string{ + flagTaskHistoryLimit: "10", + }, + swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { + return fmt.Errorf("error updating the swarm") + }, + expectedError: "error updating the swarm", + }, + { + name: "swarm-unlockkey-error", + flags: map[string]string{ + flagAutolock: "true", + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(), nil + }, + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{}, fmt.Errorf("error getting unlock key") + }, + expectedError: "error getting unlock key", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newUpdateCommand( + test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + swarmUpdateFunc: tc.swarmUpdateFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSwarmUpdate(t *testing.T) { + testCases := []struct { + name string + args []string + flags map[string]string + swarmInspectFunc func() (swarm.Swarm, error) + swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error + swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) + }{ + { + name: "noargs", + }, + { + name: "all-flags-quiet", + flags: map[string]string{ + flagTaskHistoryLimit: "10", + flagDispatcherHeartbeat: "10s", + flagCertExpiry: "20s", + flagExternalCA: "protocol=cfssl,url=https://example.com.", + flagMaxSnapshots: "10", + flagSnapshotInterval: "100", + flagAutolock: "true", + flagQuiet: "true", + }, + swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { + if *swarm.Orchestration.TaskHistoryRetentionLimit != 10 { + return fmt.Errorf("historyLimit not correctly set") + } + heartbeatDuration, err := time.ParseDuration("10s") + if err != nil { + return err + } + if swarm.Dispatcher.HeartbeatPeriod != heartbeatDuration { + return fmt.Errorf("heartbeatPeriodLimit not correctly set") + } + certExpiryDuration, err := time.ParseDuration("20s") + if err != nil { + return err + } + if swarm.CAConfig.NodeCertExpiry != certExpiryDuration { + return fmt.Errorf("certExpiry not correctly set") + } + if len(swarm.CAConfig.ExternalCAs) != 1 { + return fmt.Errorf("externalCA not correctly set") + } + if *swarm.Raft.KeepOldSnapshots != 10 { + return fmt.Errorf("keepOldSnapshots not correctly set") + } + if swarm.Raft.SnapshotInterval != 100 { + return fmt.Errorf("snapshotInterval not correctly set") + } + if !swarm.EncryptionConfig.AutoLockManagers { + return fmt.Errorf("autolock not correctly set") + } + return nil + }, + }, + { + name: "autolock-unlock-key", + flags: map[string]string{ + flagTaskHistoryLimit: "10", + flagAutolock: "true", + }, + swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { + if *swarm.Orchestration.TaskHistoryRetentionLimit != 10 { + return fmt.Errorf("historyLimit not correctly set") + } + return nil + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(), nil + }, + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{ + UnlockKey: "unlock-key", + }, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newUpdateCommand( + test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + swarmUpdateFunc: tc.swarmUpdateFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(buf) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("update-%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} diff --git a/cli/command/task/print.go b/cli/command/task/print.go index 57c4e0c8c8..60a2bca85b 100644 --- a/cli/command/task/print.go +++ b/cli/command/task/print.go @@ -61,7 +61,7 @@ func (t tasksBySlot) Less(i, j int) bool { // Print task information in a table format. // Besides this, command `docker node ps ` // and `docker stack ps` will call this, too. -func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, noTrunc bool) error { +func Print(dockerCli command.Cli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, noTrunc bool) error { sort.Stable(tasksBySlot(tasks)) writer := tabwriter.NewWriter(dockerCli.Out(), 0, 4, 2, ' ', 0) @@ -74,7 +74,7 @@ func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task } // PrintQuiet shows task list in a quiet way. -func PrintQuiet(dockerCli *command.DockerCli, tasks []swarm.Task) error { +func PrintQuiet(dockerCli command.Cli, tasks []swarm.Task) error { sort.Stable(tasksBySlot(tasks)) out := dockerCli.Out() diff --git a/cli/internal/test/builders/node.go b/cli/internal/test/builders/node.go new file mode 100644 index 0000000000..63fdebba12 --- /dev/null +++ b/cli/internal/test/builders/node.go @@ -0,0 +1,117 @@ +package builders + +import ( + "time" + + "github.com/docker/docker/api/types/swarm" +) + +// Node creates a node with default values. +// Any number of node function builder can be pass to augment it. +func Node(builders ...func(*swarm.Node)) *swarm.Node { + t1 := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) + node := &swarm.Node{ + ID: "nodeID", + Meta: swarm.Meta{ + CreatedAt: t1, + }, + Description: swarm.NodeDescription{ + Hostname: "defaultNodeHostname", + Platform: swarm.Platform{ + Architecture: "x86_64", + OS: "linux", + }, + Resources: swarm.Resources{ + NanoCPUs: 4, + MemoryBytes: 20 * 1024 * 1024, + }, + Engine: swarm.EngineDescription{ + EngineVersion: "1.13.0", + Labels: map[string]string{ + "engine": "label", + }, + Plugins: []swarm.PluginDescription{ + { + Type: "Volume", + Name: "local", + }, + { + Type: "Network", + Name: "bridge", + }, + { + Type: "Network", + Name: "overlay", + }, + }, + }, + }, + Status: swarm.NodeStatus{ + State: swarm.NodeStateReady, + Addr: "127.0.0.1", + }, + Spec: swarm.NodeSpec{ + Annotations: swarm.Annotations{ + Name: "defaultNodeName", + }, + Role: swarm.NodeRoleWorker, + Availability: swarm.NodeAvailabilityActive, + }, + } + + for _, builder := range builders { + builder(node) + } + + return node +} + +// NodeID sets the node id +func NodeID(id string) func(*swarm.Node) { + return func(node *swarm.Node) { + node.ID = id + } +} + +// NodeLabels sets the node labels +func NodeLabels(labels map[string]string) func(*swarm.Node) { + return func(node *swarm.Node) { + node.Spec.Labels = labels + } +} + +// Hostname sets the node hostname +func Hostname(hostname string) func(*swarm.Node) { + return func(node *swarm.Node) { + node.Description.Hostname = hostname + } +} + +// Leader sets the current node as a leader +func Leader() func(*swarm.ManagerStatus) { + return func(managerStatus *swarm.ManagerStatus) { + managerStatus.Leader = true + } +} + +// Manager set the current node as a manager +func Manager(managerStatusBuilders ...func(*swarm.ManagerStatus)) func(*swarm.Node) { + return func(node *swarm.Node) { + node.Spec.Role = swarm.NodeRoleManager + node.ManagerStatus = ManagerStatus(managerStatusBuilders...) + } +} + +// ManagerStatus create a ManageStatus with default values. +func ManagerStatus(managerStatusBuilders ...func(*swarm.ManagerStatus)) *swarm.ManagerStatus { + managerStatus := &swarm.ManagerStatus{ + Reachability: swarm.ReachabilityReachable, + Addr: "127.0.0.1", + } + + for _, builder := range managerStatusBuilders { + builder(managerStatus) + } + + return managerStatus +} diff --git a/cli/internal/test/builders/swarm.go b/cli/internal/test/builders/swarm.go new file mode 100644 index 0000000000..ab1a930628 --- /dev/null +++ b/cli/internal/test/builders/swarm.go @@ -0,0 +1,39 @@ +package builders + +import ( + "time" + + "github.com/docker/docker/api/types/swarm" +) + +// Swarm creates a swarm with default values. +// Any number of swarm function builder can be pass to augment it. +func Swarm(swarmBuilders ...func(*swarm.Swarm)) *swarm.Swarm { + t1 := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) + swarm := &swarm.Swarm{ + ClusterInfo: swarm.ClusterInfo{ + ID: "swarm", + Meta: swarm.Meta{ + CreatedAt: t1, + }, + Spec: swarm.Spec{}, + }, + JoinTokens: swarm.JoinTokens{ + Worker: "worker-join-token", + Manager: "manager-join-token", + }, + } + + for _, builder := range swarmBuilders { + builder(swarm) + } + + return swarm +} + +// Autolock set the swarm into autolock mode +func Autolock() func(*swarm.Swarm) { + return func(swarm *swarm.Swarm) { + swarm.Spec.EncryptionConfig.AutoLockManagers = true + } +} diff --git a/cli/internal/test/builders/task.go b/cli/internal/test/builders/task.go new file mode 100644 index 0000000000..688c62a3a8 --- /dev/null +++ b/cli/internal/test/builders/task.go @@ -0,0 +1,111 @@ +package builders + +import ( + "time" + + "github.com/docker/docker/api/types/swarm" +) + +var ( + defaultTime = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) +) + +// Task creates a task with default values . +// Any number of task function builder can be pass to augment it. +func Task(taskBuilders ...func(*swarm.Task)) *swarm.Task { + task := &swarm.Task{ + ID: "taskID", + Meta: swarm.Meta{ + CreatedAt: defaultTime, + }, + Annotations: swarm.Annotations{ + Name: "defaultTaskName", + }, + Spec: *TaskSpec(), + ServiceID: "rl02d5gwz6chzu7il5fhtb8be", + Slot: 1, + Status: *TaskStatus(), + DesiredState: swarm.TaskStateReady, + } + + for _, builder := range taskBuilders { + builder(task) + } + + return task +} + +// TaskID sets the task ID +func TaskID(id string) func(*swarm.Task) { + return func(task *swarm.Task) { + task.ID = id + } +} + +// ServiceID sets the task service's ID +func ServiceID(id string) func(*swarm.Task) { + return func(task *swarm.Task) { + task.ServiceID = id + } +} + +// WithStatus sets the task status +func WithStatus(statusBuilders ...func(*swarm.TaskStatus)) func(*swarm.Task) { + return func(task *swarm.Task) { + task.Status = *TaskStatus(statusBuilders...) + } +} + +// TaskStatus creates a task status with default values . +// Any number of taskStatus function builder can be pass to augment it. +func TaskStatus(statusBuilders ...func(*swarm.TaskStatus)) *swarm.TaskStatus { + timestamp := defaultTime.Add(1 * time.Hour) + taskStatus := &swarm.TaskStatus{ + State: swarm.TaskStateReady, + Timestamp: timestamp, + } + + for _, builder := range statusBuilders { + builder(taskStatus) + } + + return taskStatus +} + +// Timestamp sets the task status timestamp +func Timestamp(t time.Time) func(*swarm.TaskStatus) { + return func(taskStatus *swarm.TaskStatus) { + taskStatus.Timestamp = t + } +} + +// StatusErr sets the tasks status error +func StatusErr(err string) func(*swarm.TaskStatus) { + return func(taskStatus *swarm.TaskStatus) { + taskStatus.Err = err + } +} + +// PortStatus sets the tasks port config status +// FIXME(vdemeester) should be a sub builder 👼 +func PortStatus(portConfigs []swarm.PortConfig) func(*swarm.TaskStatus) { + return func(taskStatus *swarm.TaskStatus) { + taskStatus.PortStatus.Ports = portConfigs + } +} + +// 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 { + taskSpec := &swarm.TaskSpec{ + ContainerSpec: swarm.ContainerSpec{ + Image: "myimage:mytag", + }, + } + + for _, builder := range specBuilders { + builder(taskSpec) + } + + return taskSpec +} diff --git a/cli/internal/test/cli.go b/cli/internal/test/cli.go new file mode 100644 index 0000000000..06ab053e98 --- /dev/null +++ b/cli/internal/test/cli.go @@ -0,0 +1,48 @@ +// Package test is a test-only package that can be used by other cli package to write unit test +package test + +import ( + "io" + "io/ioutil" + + "github.com/docker/docker/cli/command" + "github.com/docker/docker/client" + "strings" +) + +// FakeCli emulates the default DockerCli +type FakeCli struct { + command.DockerCli + client client.APIClient + out io.Writer + in io.ReadCloser +} + +// NewFakeCli returns a Cli backed by the fakeCli +func NewFakeCli(client client.APIClient, out io.Writer) *FakeCli { + return &FakeCli{ + client: client, + out: out, + in: ioutil.NopCloser(strings.NewReader("")), + } +} + +// SetIn sets the input of the cli to the specified ReadCloser +func (c *FakeCli) SetIn(in io.ReadCloser) { + c.in = in +} + +// Client returns a docker API client +func (c *FakeCli) Client() client.APIClient { + return c.client +} + +// Out returns the output stream the cli should write on +func (c *FakeCli) Out() *command.OutStream { + return command.NewOutStream(c.out) +} + +// In returns thi input stream the cli will use +func (c *FakeCli) In() *command.InStream { + return command.NewInStream(c.in) +} diff --git a/integration-cli/docker_cli_swarm_test.go b/integration-cli/docker_cli_swarm_test.go index cf49a4dd50..3d04561c5f 100644 --- a/integration-cli/docker_cli_swarm_test.go +++ b/integration-cli/docker_cli_swarm_test.go @@ -119,16 +119,6 @@ func (s *DockerSwarmSuite) TestSwarmIncompatibleDaemon(c *check.C) { d.Start(c) } -// Test case for #24090 -func (s *DockerSwarmSuite) TestSwarmNodeListHostname(c *check.C) { - d := s.AddDaemon(c, true, true) - - // The first line should contain "HOSTNAME" - out, err := d.Cmd("node", "ls") - c.Assert(err, checker.IsNil) - c.Assert(strings.Split(out, "\n")[0], checker.Contains, "HOSTNAME") -} - func (s *DockerSwarmSuite) TestSwarmServiceTemplatingHostname(c *check.C) { d := s.AddDaemon(c, true, true) @@ -235,51 +225,23 @@ func (s *DockerSwarmSuite) TestSwarmNodeTaskListFilter(c *check.C) { func (s *DockerSwarmSuite) TestSwarmPublishAdd(c *check.C) { d := s.AddDaemon(c, true, true) - testCases := []struct { - name string - publishAdd []string - ports string - }{ - { - name: "simple-syntax", - publishAdd: []string{ - "80:80", - "80:80", - "80:80", - "80:20", - }, - ports: "[{ tcp 80 80 ingress}]", - }, - { - name: "complex-syntax", - publishAdd: []string{ - "target=90,published=90,protocol=tcp,mode=ingress", - "target=90,published=90,protocol=tcp,mode=ingress", - "target=90,published=90,protocol=tcp,mode=ingress", - "target=30,published=90,protocol=tcp,mode=ingress", - }, - ports: "[{ tcp 90 90 ingress}]", - }, - } + name := "top" + out, err := d.Cmd("service", "create", "--name", name, "--label", "x=y", "busybox", "top") + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") - for _, tc := range testCases { - out, err := d.Cmd("service", "create", "--name", tc.name, "--label", "x=y", "busybox", "top") - c.Assert(err, checker.IsNil, check.Commentf(out)) - c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") + out, err = d.Cmd("service", "update", "--publish-add", "80:80", name) + c.Assert(err, checker.IsNil) - out, err = d.CmdRetryOutOfSequence("service", "update", "--publish-add", tc.publishAdd[0], tc.name) - c.Assert(err, checker.IsNil, check.Commentf(out)) + out, err = d.CmdRetryOutOfSequence("service", "update", "--publish-add", "80:80", name) + c.Assert(err, checker.IsNil) - out, err = d.CmdRetryOutOfSequence("service", "update", "--publish-add", tc.publishAdd[1], tc.name) - c.Assert(err, checker.IsNil, check.Commentf(out)) + out, err = d.CmdRetryOutOfSequence("service", "update", "--publish-add", "80:80", "--publish-add", "80:20", name) + c.Assert(err, checker.NotNil) - out, err = d.CmdRetryOutOfSequence("service", "update", "--publish-add", tc.publishAdd[2], "--publish-add", tc.publishAdd[3], tc.name) - c.Assert(err, checker.NotNil, check.Commentf(out)) - - out, err = d.Cmd("service", "inspect", "--format", "{{ .Spec.EndpointSpec.Ports }}", tc.name) - c.Assert(err, checker.IsNil) - c.Assert(strings.TrimSpace(out), checker.Equals, tc.ports) - } + out, err = d.Cmd("service", "inspect", "--format", "{{ .Spec.EndpointSpec.Ports }}", name) + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Equals, "[{ tcp 80 80 ingress}]") } func (s *DockerSwarmSuite) TestSwarmServiceWithGroup(c *check.C) { @@ -1413,24 +1375,6 @@ func (s *DockerSwarmSuite) TestSwarmNetworkIPAMOptions(c *check.C) { c.Assert(strings.TrimSpace(out), checker.Equals, "map[foo:bar]") } -// TODO: migrate to a unit test -// This test could be migrated to unit test and save costly integration test, -// once PR #29143 is merged. -func (s *DockerSwarmSuite) TestSwarmUpdateWithoutArgs(c *check.C) { - d := s.AddDaemon(c, true, true) - - expectedOutput := ` -Usage: docker swarm update [OPTIONS] - -Update the swarm - -Options:` - - out, err := d.Cmd("swarm", "update") - c.Assert(err, checker.IsNil, check.Commentf("out: %v", out)) - c.Assert(out, checker.Contains, expectedOutput, check.Commentf(out)) -} - func (s *DockerTrustedSwarmSuite) TestTrustedServiceCreate(c *check.C) { d := s.swarmSuite.AddDaemon(c, true, true) diff --git a/pkg/testutil/assert/assert.go b/pkg/testutil/assert/assert.go index 6da8518a5e..86736d7c7d 100644 --- a/pkg/testutil/assert/assert.go +++ b/pkg/testutil/assert/assert.go @@ -7,6 +7,7 @@ import ( "reflect" "runtime" "strings" + "unicode" "github.com/davecgh/go-spew/spew" ) @@ -25,6 +26,25 @@ func Equal(t TestingT, actual, expected interface{}) { } } +// EqualNormalizedString compare the actual value to the expected value after applying the specified +// transform function. It fails the test if these two transformed string are not equal. +// For example `EqualNormalizedString(t, RemoveSpace, "foo\n", "foo")` wouldn't fail the test as +// spaces (and thus '\n') are removed before comparing the string. +func EqualNormalizedString(t TestingT, transformFun func(rune) rune, actual, expected string) { + if strings.Map(transformFun, actual) != strings.Map(transformFun, expected) { + fatal(t, "Expected '%v' got '%v'", expected, expected, actual, actual) + } +} + +// RemoveSpace returns -1 if the specified runes is considered as a space (unicode) +// and the rune itself otherwise. +func RemoveSpace(r rune) rune { + if unicode.IsSpace(r) { + return -1 + } + return r +} + //EqualStringSlice compares two slices and fails the test if they do not contain // the same items. func EqualStringSlice(t TestingT, actual, expected []string) { diff --git a/pkg/testutil/golden/golden.go b/pkg/testutil/golden/golden.go new file mode 100644 index 0000000000..8f725da7bf --- /dev/null +++ b/pkg/testutil/golden/golden.go @@ -0,0 +1,28 @@ +// Package golden provides function and helpers to use golden file for +// testing purpose. +package golden + +import ( + "flag" + "io/ioutil" + "path/filepath" + "testing" +) + +var update = flag.Bool("test.update", false, "update golden file") + +// Get returns the golden file content. If the `test.update` is specified, it updates the +// file with the current output and returns it. +func Get(t *testing.T, actual []byte, filename string) []byte { + golden := filepath.Join("testdata", filename) + if *update { + if err := ioutil.WriteFile(golden, actual, 0644); err != nil { + t.Fatal(err) + } + } + expected, err := ioutil.ReadFile(golden) + if err != nil { + t.Fatal(err) + } + return expected +}