diff --git a/command/volume/client_test.go b/command/volume/client_test.go new file mode 100644 index 0000000000..c29655cdb0 --- /dev/null +++ b/command/volume/client_test.go @@ -0,0 +1,53 @@ +package volume + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + volumetypes "github.com/docker/docker/api/types/volume" + "github.com/docker/docker/client" + "golang.org/x/net/context" +) + +type fakeClient struct { + client.Client + volumeCreateFunc func(volumetypes.VolumesCreateBody) (types.Volume, error) + volumeInspectFunc func(volumeID string) (types.Volume, error) + volumeListFunc func(filter filters.Args) (volumetypes.VolumesListOKBody, error) + volumeRemoveFunc func(volumeID string, force bool) error + volumePruneFunc func(filter filters.Args) (types.VolumesPruneReport, error) +} + +func (c *fakeClient) VolumeCreate(ctx context.Context, options volumetypes.VolumesCreateBody) (types.Volume, error) { + if c.volumeCreateFunc != nil { + return c.volumeCreateFunc(options) + } + return types.Volume{}, nil +} + +func (c *fakeClient) VolumeInspect(ctx context.Context, volumeID string) (types.Volume, error) { + if c.volumeInspectFunc != nil { + return c.volumeInspectFunc(volumeID) + } + return types.Volume{}, nil +} + +func (c *fakeClient) VolumeList(ctx context.Context, filter filters.Args) (volumetypes.VolumesListOKBody, error) { + if c.volumeListFunc != nil { + return c.volumeListFunc(filter) + } + return volumetypes.VolumesListOKBody{}, nil +} + +func (c *fakeClient) VolumesPrune(ctx context.Context, filter filters.Args) (types.VolumesPruneReport, error) { + if c.volumePruneFunc != nil { + return c.volumePruneFunc(filter) + } + return types.VolumesPruneReport{}, nil +} + +func (c *fakeClient) VolumeRemove(ctx context.Context, volumeID string, force bool) error { + if c.volumeRemoveFunc != nil { + return c.volumeRemoveFunc(volumeID, force) + } + return nil +} diff --git a/command/volume/cmd.go b/command/volume/cmd.go index 2bc7687750..4ef8381333 100644 --- a/command/volume/cmd.go +++ b/command/volume/cmd.go @@ -1,10 +1,9 @@ package volume import ( - "github.com/spf13/cobra" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" ) // NewVolumeCommand returns a cobra command for `volume` subcommands diff --git a/command/volume/create.go b/command/volume/create.go index 21cfa84b7b..f7ca362150 100644 --- a/command/volume/create.go +++ b/command/volume/create.go @@ -19,7 +19,7 @@ type createOptions struct { labels opts.ListOpts } -func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { +func newCreateCommand(dockerCli command.Cli) *cobra.Command { opts := createOptions{ driverOpts: *opts.NewMapOpts(nil, nil), labels: opts.NewListOpts(opts.ValidateEnv), @@ -32,8 +32,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 1 { if opts.name != "" { - fmt.Fprint(dockerCli.Err(), "Conflicting options: either specify --name or provide positional arg, not both\n") - return cli.StatusError{StatusCode: 1} + return fmt.Errorf("Conflicting options: either specify --name or provide positional arg, not both\n") } opts.name = args[0] } @@ -50,7 +49,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runCreate(dockerCli *command.DockerCli, opts createOptions) error { +func runCreate(dockerCli command.Cli, opts createOptions) error { client := dockerCli.Client() volReq := volumetypes.VolumesCreateBody{ diff --git a/command/volume/create_test.go b/command/volume/create_test.go new file mode 100644 index 0000000000..b7d5a443a5 --- /dev/null +++ b/command/volume/create_test.go @@ -0,0 +1,142 @@ +package volume + +import ( + "bytes" + "fmt" + "io/ioutil" + "strings" + "testing" + + "github.com/docker/docker/api/types" + volumetypes "github.com/docker/docker/api/types/volume" + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestVolumeCreateErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + volumeCreateFunc func(volumetypes.VolumesCreateBody) (types.Volume, error) + expectedError string + }{ + { + args: []string{"volumeName"}, + flags: map[string]string{ + "name": "volumeName", + }, + expectedError: "Conflicting options: either specify --name or provide positional arg, not both", + }, + { + args: []string{"too", "many"}, + expectedError: "requires at most 1 argument(s)", + }, + { + volumeCreateFunc: func(createBody volumetypes.VolumesCreateBody) (types.Volume, error) { + return types.Volume{}, fmt.Errorf("error creating volume") + }, + expectedError: "error creating volume", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newCreateCommand( + test.NewFakeCli(&fakeClient{ + volumeCreateFunc: tc.volumeCreateFunc, + }, 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 TestVolumeCreateWithName(t *testing.T) { + name := "foo" + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + volumeCreateFunc: func(body volumetypes.VolumesCreateBody) (types.Volume, error) { + if body.Name != name { + return types.Volume{}, fmt.Errorf("expected name %q, got %q", name, body.Name) + } + return types.Volume{ + Name: body.Name, + }, nil + }, + }, buf) + + // Test by flags + cmd := newCreateCommand(cli) + cmd.Flags().Set("name", name) + assert.NilError(t, cmd.Execute()) + assert.Equal(t, strings.TrimSpace(buf.String()), name) + + // Then by args + buf.Reset() + cmd = newCreateCommand(cli) + cmd.SetArgs([]string{name}) + assert.NilError(t, cmd.Execute()) + assert.Equal(t, strings.TrimSpace(buf.String()), name) +} + +func TestVolumeCreateWithFlags(t *testing.T) { + expectedDriver := "foo" + expectedOpts := map[string]string{ + "bar": "1", + "baz": "baz", + } + expectedLabels := map[string]string{ + "lbl1": "v1", + "lbl2": "v2", + } + name := "banana" + + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + volumeCreateFunc: func(body volumetypes.VolumesCreateBody) (types.Volume, error) { + if body.Name != "" { + return types.Volume{}, fmt.Errorf("expected empty name, got %q", body.Name) + } + if body.Driver != expectedDriver { + return types.Volume{}, fmt.Errorf("expected driver %q, got %q", expectedDriver, body.Driver) + } + if !compareMap(body.DriverOpts, expectedOpts) { + return types.Volume{}, fmt.Errorf("expected drivers opts %v, got %v", expectedOpts, body.DriverOpts) + } + if !compareMap(body.Labels, expectedLabels) { + return types.Volume{}, fmt.Errorf("expected labels %v, got %v", expectedLabels, body.Labels) + } + return types.Volume{ + Name: name, + }, nil + }, + }, buf) + + cmd := newCreateCommand(cli) + cmd.Flags().Set("driver", "foo") + cmd.Flags().Set("opt", "bar=1") + cmd.Flags().Set("opt", "baz=baz") + cmd.Flags().Set("label", "lbl1=v1") + cmd.Flags().Set("label", "lbl2=v2") + assert.NilError(t, cmd.Execute()) + assert.Equal(t, strings.TrimSpace(buf.String()), name) +} + +func compareMap(actual map[string]string, expected map[string]string) bool { + if len(actual) != len(expected) { + return false + } + for key, value := range actual { + if expectedValue, ok := expected[key]; ok { + if expectedValue != value { + return false + } + } else { + return false + } + } + return true +} diff --git a/command/volume/inspect.go b/command/volume/inspect.go index f58b927ace..70db264951 100644 --- a/command/volume/inspect.go +++ b/command/volume/inspect.go @@ -1,12 +1,11 @@ package volume import ( - "golang.org/x/net/context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/inspect" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type inspectOptions struct { @@ -14,7 +13,7 @@ type inspectOptions struct { names []string } -func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { +func newInspectCommand(dockerCli command.Cli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ @@ -32,7 +31,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() diff --git a/command/volume/inspect_test.go b/command/volume/inspect_test.go new file mode 100644 index 0000000000..e2ea7b35de --- /dev/null +++ b/command/volume/inspect_test.go @@ -0,0 +1,150 @@ +package volume + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types" + "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 TestVolumeInspectErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + volumeInspectFunc func(volumeID string) (types.Volume, error) + expectedError string + }{ + { + expectedError: "requires at least 1 argument", + }, + { + args: []string{"foo"}, + volumeInspectFunc: func(volumeID string) (types.Volume, error) { + return types.Volume{}, fmt.Errorf("error while inspecting the volume") + }, + expectedError: "error while inspecting the volume", + }, + { + args: []string{"foo"}, + flags: map[string]string{ + "format": "{{invalid format}}", + }, + expectedError: "Template parsing error", + }, + { + args: []string{"foo", "bar"}, + volumeInspectFunc: func(volumeID string) (types.Volume, error) { + if volumeID == "foo" { + return types.Volume{ + Name: "foo", + }, nil + } + return types.Volume{}, fmt.Errorf("error while inspecting the volume") + }, + expectedError: "error while inspecting the volume", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newInspectCommand( + test.NewFakeCli(&fakeClient{ + volumeInspectFunc: tc.volumeInspectFunc, + }, 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 TestVolumeInspectWithoutFormat(t *testing.T) { + testCases := []struct { + name string + args []string + volumeInspectFunc func(volumeID string) (types.Volume, error) + }{ + { + name: "single-volume", + args: []string{"foo"}, + volumeInspectFunc: func(volumeID string) (types.Volume, error) { + if volumeID != "foo" { + return types.Volume{}, fmt.Errorf("Invalid volumeID, expected %s, got %s", "foo", volumeID) + } + return *Volume(), nil + }, + }, + { + name: "multiple-volume-with-labels", + args: []string{"foo", "bar"}, + volumeInspectFunc: func(volumeID string) (types.Volume, error) { + return *Volume(VolumeName(volumeID), VolumeLabels(map[string]string{ + "foo": "bar", + })), nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newInspectCommand( + test.NewFakeCli(&fakeClient{ + volumeInspectFunc: tc.volumeInspectFunc, + }, buf), + ) + cmd.SetArgs(tc.args) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("volume-inspect-without-format.%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} + +func TestVolumeInspectWithFormat(t *testing.T) { + volumeInspectFunc := func(volumeID string) (types.Volume, error) { + return *Volume(VolumeLabels(map[string]string{ + "foo": "bar", + })), nil + } + testCases := []struct { + name string + format string + args []string + volumeInspectFunc func(volumeID string) (types.Volume, error) + }{ + { + name: "simple-template", + format: "{{.Name}}", + args: []string{"foo"}, + volumeInspectFunc: volumeInspectFunc, + }, + { + name: "json-template", + format: "{{json .Labels}}", + args: []string{"foo"}, + volumeInspectFunc: volumeInspectFunc, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newInspectCommand( + test.NewFakeCli(&fakeClient{ + volumeInspectFunc: tc.volumeInspectFunc, + }, buf), + ) + cmd.SetArgs(tc.args) + cmd.Flags().Set("format", tc.format) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("volume-inspect-with-format.%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} diff --git a/command/volume/list.go b/command/volume/list.go index 0de83aea4e..3577db9554 100644 --- a/command/volume/list.go +++ b/command/volume/list.go @@ -3,14 +3,13 @@ package volume import ( "sort" - "golang.org/x/net/context" - "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/formatter" "github.com/docker/docker/opts" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type byVolumeName []*types.Volume @@ -27,7 +26,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{ @@ -48,7 +47,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() volumes, err := client.VolumeList(context.Background(), opts.filter.Value()) if err != nil { diff --git a/command/volume/list_test.go b/command/volume/list_test.go new file mode 100644 index 0000000000..2f4a366333 --- /dev/null +++ b/command/volume/list_test.go @@ -0,0 +1,124 @@ +package volume + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + volumetypes "github.com/docker/docker/api/types/volume" + "github.com/docker/docker/cli/config/configfile" + "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 TestVolumeListErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + volumeListFunc func(filter filters.Args) (volumetypes.VolumesListOKBody, error) + expectedError string + }{ + { + args: []string{"foo"}, + expectedError: "accepts no argument", + }, + { + volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { + return volumetypes.VolumesListOKBody{}, fmt.Errorf("error listing volumes") + }, + expectedError: "error listing volumes", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newListCommand( + test.NewFakeCli(&fakeClient{ + volumeListFunc: tc.volumeListFunc, + }, 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 TestVolumeListWithoutFormat(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { + return volumetypes.VolumesListOKBody{ + Volumes: []*types.Volume{ + Volume(), + Volume(VolumeName("foo"), VolumeDriver("bar")), + Volume(VolumeName("baz"), VolumeLabels(map[string]string{ + "foo": "bar", + })), + }, + }, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newListCommand(cli) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "volume-list-without-format.golden") + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) +} + +func TestVolumeListWithConfigFormat(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { + return volumetypes.VolumesListOKBody{ + Volumes: []*types.Volume{ + Volume(), + Volume(VolumeName("foo"), VolumeDriver("bar")), + Volume(VolumeName("baz"), VolumeLabels(map[string]string{ + "foo": "bar", + })), + }, + }, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{ + VolumesFormat: "{{ .Name }} {{ .Driver }} {{ .Labels }}", + }) + cmd := newListCommand(cli) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "volume-list-with-config-format.golden") + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) +} + +func TestVolumeListWithFormat(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { + return volumetypes.VolumesListOKBody{ + Volumes: []*types.Volume{ + Volume(), + Volume(VolumeName("foo"), VolumeDriver("bar")), + Volume(VolumeName("baz"), VolumeLabels(map[string]string{ + "foo": "bar", + })), + }, + }, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newListCommand(cli) + cmd.Flags().Set("format", "{{ .Name }} {{ .Driver }} {{ .Labels }}") + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "volume-list-with-format.golden") + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) +} diff --git a/command/volume/prune.go b/command/volume/prune.go index 405fbeb295..7e78c66e07 100644 --- a/command/volume/prune.go +++ b/command/volume/prune.go @@ -3,13 +3,12 @@ package volume import ( "fmt" - "golang.org/x/net/context" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" units "github.com/docker/go-units" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type pruneOptions struct { @@ -17,7 +16,7 @@ type pruneOptions struct { } // NewPruneCommand returns a new cobra prune command for volumes -func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { +func NewPruneCommand(dockerCli command.Cli) *cobra.Command { var opts pruneOptions cmd := &cobra.Command{ @@ -47,7 +46,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { const warning = `WARNING! This will remove all volumes not used by at least one container. Are you sure you want to continue?` -func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { +func runPrune(dockerCli command.Cli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { return } diff --git a/command/volume/prune_test.go b/command/volume/prune_test.go new file mode 100644 index 0000000000..c07834675e --- /dev/null +++ b/command/volume/prune_test.go @@ -0,0 +1,132 @@ +package volume + +import ( + "bytes" + "fmt" + "io/ioutil" + "runtime" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestVolumePruneErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + volumePruneFunc func(args filters.Args) (types.VolumesPruneReport, error) + expectedError string + }{ + { + args: []string{"foo"}, + expectedError: "accepts no argument", + }, + { + flags: map[string]string{ + "force": "true", + }, + volumePruneFunc: func(args filters.Args) (types.VolumesPruneReport, error) { + return types.VolumesPruneReport{}, fmt.Errorf("error pruning volumes") + }, + expectedError: "error pruning volumes", + }, + } + for _, tc := range testCases { + cmd := NewPruneCommand( + test.NewFakeCli(&fakeClient{ + volumePruneFunc: tc.volumePruneFunc, + }, ioutil.Discard), + ) + 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 TestVolumePruneForce(t *testing.T) { + testCases := []struct { + name string + volumePruneFunc func(args filters.Args) (types.VolumesPruneReport, error) + }{ + { + name: "empty", + }, + { + name: "deletedVolumes", + volumePruneFunc: simplePruneFunc, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := NewPruneCommand( + test.NewFakeCli(&fakeClient{ + volumePruneFunc: tc.volumePruneFunc, + }, buf), + ) + cmd.Flags().Set("force", "true") + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("volume-prune.%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} +func TestVolumePrunePromptYes(t *testing.T) { + if runtime.GOOS == "windows" { + // FIXME(vdemeester) make it work.. + t.Skip("skipping this test on Windows") + } + for _, input := range []string{"y", "Y"} { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + volumePruneFunc: simplePruneFunc, + }, buf) + + cli.SetIn(ioutil.NopCloser(strings.NewReader(input))) + cmd := NewPruneCommand( + cli, + ) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "volume-prune-yes.golden") + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} + +func TestVolumePrunePromptNo(t *testing.T) { + if runtime.GOOS == "windows" { + // FIXME(vdemeester) make it work.. + t.Skip("skipping this test on Windows") + } + for _, input := range []string{"n", "N", "no", "anything", "really"} { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + volumePruneFunc: simplePruneFunc, + }, buf) + + cli.SetIn(ioutil.NopCloser(strings.NewReader(input))) + cmd := NewPruneCommand( + cli, + ) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "volume-prune-no.golden") + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} + +func simplePruneFunc(args filters.Args) (types.VolumesPruneReport, error) { + return types.VolumesPruneReport{ + VolumesDeleted: []string{ + "foo", "bar", "baz", + }, + SpaceReclaimed: 2000, + }, nil +} diff --git a/command/volume/remove.go b/command/volume/remove.go index f464bb3e1a..c1267f1eab 100644 --- a/command/volume/remove.go +++ b/command/volume/remove.go @@ -2,12 +2,12 @@ package volume import ( "fmt" - - "golang.org/x/net/context" + "strings" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type removeOptions struct { @@ -16,7 +16,7 @@ type removeOptions struct { volumes []string } -func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { +func newRemoveCommand(dockerCli command.Cli) *cobra.Command { var opts removeOptions cmd := &cobra.Command{ @@ -38,22 +38,22 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runRemove(dockerCli *command.DockerCli, opts *removeOptions) error { +func runRemove(dockerCli command.Cli, opts *removeOptions) error { client := dockerCli.Client() ctx := context.Background() - status := 0 + + var errs []string for _, name := range opts.volumes { if err := client.VolumeRemove(ctx, name, opts.force); err != nil { - fmt.Fprintf(dockerCli.Err(), "%s\n", err) - status = 1 + errs = append(errs, err.Error()) continue } fmt.Fprintf(dockerCli.Out(), "%s\n", name) } - if status != 0 { - return cli.StatusError{StatusCode: status} + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) } return nil } diff --git a/command/volume/remove_test.go b/command/volume/remove_test.go new file mode 100644 index 0000000000..b2a106c22d --- /dev/null +++ b/command/volume/remove_test.go @@ -0,0 +1,47 @@ +package volume + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestVolumeRemoveErrors(t *testing.T) { + testCases := []struct { + args []string + volumeRemoveFunc func(volumeID string, force bool) error + expectedError string + }{ + { + expectedError: "requires at least 1 argument", + }, + { + args: []string{"nodeID"}, + volumeRemoveFunc: func(volumeID string, force bool) error { + return fmt.Errorf("error removing the volume") + }, + expectedError: "error removing the volume", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newRemoveCommand( + test.NewFakeCli(&fakeClient{ + volumeRemoveFunc: tc.volumeRemoveFunc, + }, 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{"volume1", "volume2"}) + assert.NilError(t, cmd.Execute()) +} diff --git a/command/volume/testdata/volume-inspect-with-format.json-template.golden b/command/volume/testdata/volume-inspect-with-format.json-template.golden new file mode 100644 index 0000000000..2393cd01d4 --- /dev/null +++ b/command/volume/testdata/volume-inspect-with-format.json-template.golden @@ -0,0 +1 @@ +{"foo":"bar"} diff --git a/command/volume/testdata/volume-inspect-with-format.simple-template.golden b/command/volume/testdata/volume-inspect-with-format.simple-template.golden new file mode 100644 index 0000000000..4833bbb039 --- /dev/null +++ b/command/volume/testdata/volume-inspect-with-format.simple-template.golden @@ -0,0 +1 @@ +volume diff --git a/command/volume/testdata/volume-inspect-without-format.multiple-volume-with-labels.golden b/command/volume/testdata/volume-inspect-without-format.multiple-volume-with-labels.golden new file mode 100644 index 0000000000..19cad5024c --- /dev/null +++ b/command/volume/testdata/volume-inspect-without-format.multiple-volume-with-labels.golden @@ -0,0 +1,22 @@ +[ + { + "Driver": "local", + "Labels": { + "foo": "bar" + }, + "Mountpoint": "/data/volume", + "Name": "foo", + "Options": null, + "Scope": "local" + }, + { + "Driver": "local", + "Labels": { + "foo": "bar" + }, + "Mountpoint": "/data/volume", + "Name": "bar", + "Options": null, + "Scope": "local" + } +] diff --git a/command/volume/testdata/volume-inspect-without-format.single-volume.golden b/command/volume/testdata/volume-inspect-without-format.single-volume.golden new file mode 100644 index 0000000000..22d0c5a659 --- /dev/null +++ b/command/volume/testdata/volume-inspect-without-format.single-volume.golden @@ -0,0 +1,10 @@ +[ + { + "Driver": "local", + "Labels": null, + "Mountpoint": "/data/volume", + "Name": "volume", + "Options": null, + "Scope": "local" + } +] diff --git a/command/volume/testdata/volume-list-with-config-format.golden b/command/volume/testdata/volume-list-with-config-format.golden new file mode 100644 index 0000000000..72fa0bd4d7 --- /dev/null +++ b/command/volume/testdata/volume-list-with-config-format.golden @@ -0,0 +1,3 @@ +baz local foo=bar +foo bar +volume local diff --git a/command/volume/testdata/volume-list-with-format.golden b/command/volume/testdata/volume-list-with-format.golden new file mode 100644 index 0000000000..72fa0bd4d7 --- /dev/null +++ b/command/volume/testdata/volume-list-with-format.golden @@ -0,0 +1,3 @@ +baz local foo=bar +foo bar +volume local diff --git a/command/volume/testdata/volume-list-without-format.golden b/command/volume/testdata/volume-list-without-format.golden new file mode 100644 index 0000000000..9cf779e827 --- /dev/null +++ b/command/volume/testdata/volume-list-without-format.golden @@ -0,0 +1,4 @@ +DRIVER VOLUME NAME +local baz +bar foo +local volume diff --git a/command/volume/testdata/volume-prune-no.golden b/command/volume/testdata/volume-prune-no.golden new file mode 100644 index 0000000000..df5a315973 --- /dev/null +++ b/command/volume/testdata/volume-prune-no.golden @@ -0,0 +1,2 @@ +WARNING! This will remove all volumes not used by at least one container. +Are you sure you want to continue? [y/N] Total reclaimed space: 0B diff --git a/command/volume/testdata/volume-prune-yes.golden b/command/volume/testdata/volume-prune-yes.golden new file mode 100644 index 0000000000..9f6054e92a --- /dev/null +++ b/command/volume/testdata/volume-prune-yes.golden @@ -0,0 +1,7 @@ +WARNING! This will remove all volumes not used by at least one container. +Are you sure you want to continue? [y/N] Deleted Volumes: +foo +bar +baz + +Total reclaimed space: 2kB diff --git a/command/volume/testdata/volume-prune.deletedVolumes.golden b/command/volume/testdata/volume-prune.deletedVolumes.golden new file mode 100644 index 0000000000..fbe996c74d --- /dev/null +++ b/command/volume/testdata/volume-prune.deletedVolumes.golden @@ -0,0 +1,6 @@ +Deleted Volumes: +foo +bar +baz + +Total reclaimed space: 2kB diff --git a/command/volume/testdata/volume-prune.empty.golden b/command/volume/testdata/volume-prune.empty.golden new file mode 100644 index 0000000000..6c537e1ac2 --- /dev/null +++ b/command/volume/testdata/volume-prune.empty.golden @@ -0,0 +1 @@ +Total reclaimed space: 0B diff --git a/internal/test/builders/doc.go b/internal/test/builders/doc.go new file mode 100644 index 0000000000..eac991c2e4 --- /dev/null +++ b/internal/test/builders/doc.go @@ -0,0 +1,3 @@ +// Package builders helps you create struct for your unit test while keeping them expressive. +// +package builders diff --git a/internal/test/builders/node.go b/internal/test/builders/node.go index 63fdebba12..040955785c 100644 --- a/internal/test/builders/node.go +++ b/internal/test/builders/node.go @@ -8,6 +8,9 @@ import ( // Node creates a node with default values. // Any number of node function builder can be pass to augment it. +// +// n1 := Node() // Returns a default node +// n2 := Node(NodeID("foo"), NodeHostname("bar"), Leader()) func Node(builders ...func(*swarm.Node)) *swarm.Node { t1 := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) node := &swarm.Node{ diff --git a/internal/test/builders/volume.go b/internal/test/builders/volume.go new file mode 100644 index 0000000000..9b84df4238 --- /dev/null +++ b/internal/test/builders/volume.go @@ -0,0 +1,43 @@ +package builders + +import ( + "github.com/docker/docker/api/types" +) + +// Volume creates a volume with default values. +// Any number of volume function builder can be pass to augment it. +func Volume(builders ...func(volume *types.Volume)) *types.Volume { + volume := &types.Volume{ + Name: "volume", + Driver: "local", + Mountpoint: "/data/volume", + Scope: "local", + } + + for _, builder := range builders { + builder(volume) + } + + return volume +} + +// VolumeLabels sets the volume labels +func VolumeLabels(labels map[string]string) func(volume *types.Volume) { + return func(volume *types.Volume) { + volume.Labels = labels + } +} + +// VolumeName sets the volume labels +func VolumeName(name string) func(volume *types.Volume) { + return func(volume *types.Volume) { + volume.Name = name + } +} + +// VolumeDriver sets the volume driver +func VolumeDriver(name string) func(volume *types.Volume) { + return func(volume *types.Volume) { + volume.Driver = name + } +} diff --git a/internal/test/cli.go b/internal/test/cli.go index 06ab053e98..72de42586d 100644 --- a/internal/test/cli.go +++ b/internal/test/cli.go @@ -1,21 +1,23 @@ -// 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" + "strings" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/config/configfile" "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 + client client.APIClient + configfile *configfile.ConfigFile + out io.Writer + err io.Writer + in io.ReadCloser } // NewFakeCli returns a Cli backed by the fakeCli @@ -23,6 +25,7 @@ func NewFakeCli(client client.APIClient, out io.Writer) *FakeCli { return &FakeCli{ client: client, out: out, + err: ioutil.Discard, in: ioutil.NopCloser(strings.NewReader("")), } } @@ -32,17 +35,37 @@ func (c *FakeCli) SetIn(in io.ReadCloser) { c.in = in } +// SetErr sets the standard error stream th cli should write on +func (c *FakeCli) SetErr(err io.Writer) { + c.err = err +} + +// SetConfigfile sets the "fake" config file +func (c *FakeCli) SetConfigfile(configfile *configfile.ConfigFile) { + c.configfile = configfile +} + // 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 +// Out returns the output stream (stdout) 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 +// Err returns the output stream (stderr) the cli should write on +func (c *FakeCli) Err() io.Writer { + return c.err +} + +// In returns the input stream the cli will use func (c *FakeCli) In() *command.InStream { return command.NewInStream(c.in) } + +// ConfigFile returns the cli configfile object (to get client configuration) +func (c *FakeCli) ConfigFile() *configfile.ConfigFile { + return c.configfile +} diff --git a/internal/test/doc.go b/internal/test/doc.go new file mode 100644 index 0000000000..41601bd8f1 --- /dev/null +++ b/internal/test/doc.go @@ -0,0 +1,5 @@ +// Package test is a test-only package that can be used by other cli package to write unit test. +// +// It as an internal package and cannot be used outside of github.com/docker/docker/cli package. +// +package test