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 +}