From e7793092a24a38df7a6f76381eab6ac1c2f5c0b0 Mon Sep 17 00:00:00 2001 From: Ignacio Capurro Date: Thu, 30 Mar 2017 21:21:14 -0300 Subject: [PATCH] Unit tests for cli/commands/image (except build and tag) Signed-off-by: Ignacio Capurro --- cli/command/cli.go | 7 ++ cli/command/image/client_test.go | 116 ++++++++++++++++++ cli/command/image/history.go | 4 +- cli/command/image/history_test.go | 108 ++++++++++++++++ cli/command/image/import.go | 4 +- cli/command/image/import_test.go | 100 +++++++++++++++ cli/command/image/inspect.go | 4 +- cli/command/image/inspect_test.go | 92 ++++++++++++++ cli/command/image/list.go | 6 +- cli/command/image/list_test.go | 102 +++++++++++++++ cli/command/image/load.go | 4 +- cli/command/image/load_test.go | 106 ++++++++++++++++ cli/command/image/prune.go | 6 +- cli/command/image/prune_test.go | 100 +++++++++++++++ cli/command/image/pull.go | 4 +- cli/command/image/pull_test.go | 85 +++++++++++++ cli/command/image/push.go | 4 +- cli/command/image/push_test.go | 84 +++++++++++++ cli/command/image/remove.go | 6 +- cli/command/image/remove_test.go | 103 ++++++++++++++++ cli/command/image/save.go | 4 +- cli/command/image/save_test.go | 98 +++++++++++++++ cli/command/image/tag.go | 4 +- cli/command/image/tag_test.go | 43 +++++++ ...tory-command-success.quiet-no-trunc.golden | 1 + .../history-command-success.quiet.golden | 1 + .../history-command-success.simple.golden | 2 + .../testdata/import-command-success.input.txt | 1 + .../inspect-command-success.format.golden | 1 + ...inspect-command-success.simple-many.golden | 50 ++++++++ .../inspect-command-success.simple.golden | 26 ++++ .../list-command-success.filters.golden | 1 + .../list-command-success.format.golden | 0 .../list-command-success.match-name.golden | 1 + .../list-command-success.quiet-format.golden | 0 .../list-command-success.simple.golden | 1 + .../load-command-success.input-file.golden | 1 + .../testdata/load-command-success.input.txt | 1 + .../testdata/load-command-success.json.golden | 1 + .../load-command-success.simple.golden | 1 + .../testdata/prune-command-success.all.golden | 2 + ...prune-command-success.force-deleted.golden | 4 + ...rune-command-success.force-untagged.golden | 4 + .../pull-command-success.simple-no-tag.golden | 1 + .../pull-command-success.simple.golden | 0 ...-success.Image Deleted and Untagged.golden | 4 + ...emove-command-success.Image Deleted.golden | 2 + ...move-command-success.Image Untagged.golden | 2 + cli/command/image/trust.go | 14 +-- cli/command/in.go | 42 +------ cli/command/out.go | 38 +----- cli/command/registry.go | 14 +-- cli/command/stream.go | 44 +++++++ cli/command/swarm/unlock_test.go | 3 +- cli/command/volume/prune_test.go | 5 +- cli/internal/test/cli.go | 24 ++-- cli/internal/test/store.go | 74 +++++++++++ 57 files changed, 1441 insertions(+), 119 deletions(-) create mode 100644 cli/command/image/client_test.go create mode 100644 cli/command/image/history_test.go create mode 100644 cli/command/image/import_test.go create mode 100644 cli/command/image/inspect_test.go create mode 100644 cli/command/image/list_test.go create mode 100644 cli/command/image/load_test.go create mode 100644 cli/command/image/prune_test.go create mode 100644 cli/command/image/pull_test.go create mode 100644 cli/command/image/push_test.go create mode 100644 cli/command/image/remove_test.go create mode 100644 cli/command/image/save_test.go create mode 100644 cli/command/image/tag_test.go create mode 100644 cli/command/image/testdata/history-command-success.quiet-no-trunc.golden create mode 100644 cli/command/image/testdata/history-command-success.quiet.golden create mode 100644 cli/command/image/testdata/history-command-success.simple.golden create mode 100644 cli/command/image/testdata/import-command-success.input.txt create mode 100644 cli/command/image/testdata/inspect-command-success.format.golden create mode 100644 cli/command/image/testdata/inspect-command-success.simple-many.golden create mode 100644 cli/command/image/testdata/inspect-command-success.simple.golden create mode 100644 cli/command/image/testdata/list-command-success.filters.golden create mode 100644 cli/command/image/testdata/list-command-success.format.golden create mode 100644 cli/command/image/testdata/list-command-success.match-name.golden create mode 100644 cli/command/image/testdata/list-command-success.quiet-format.golden create mode 100644 cli/command/image/testdata/list-command-success.simple.golden create mode 100644 cli/command/image/testdata/load-command-success.input-file.golden create mode 100644 cli/command/image/testdata/load-command-success.input.txt create mode 100644 cli/command/image/testdata/load-command-success.json.golden create mode 100644 cli/command/image/testdata/load-command-success.simple.golden create mode 100644 cli/command/image/testdata/prune-command-success.all.golden create mode 100644 cli/command/image/testdata/prune-command-success.force-deleted.golden create mode 100644 cli/command/image/testdata/prune-command-success.force-untagged.golden create mode 100644 cli/command/image/testdata/pull-command-success.simple-no-tag.golden create mode 100644 cli/command/image/testdata/pull-command-success.simple.golden create mode 100644 cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden create mode 100644 cli/command/image/testdata/remove-command-success.Image Deleted.golden create mode 100644 cli/command/image/testdata/remove-command-success.Image Untagged.golden create mode 100644 cli/command/stream.go create mode 100644 cli/internal/test/store.go diff --git a/cli/command/cli.go b/cli/command/cli.go index c5541978c2..6cad5a10c7 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -39,7 +39,9 @@ type Cli interface { Out() *OutStream Err() io.Writer In() *InStream + SetIn(in *InStream) ConfigFile() *configfile.ConfigFile + CredentialsStore(serverAddress string) credentials.Store } // DockerCli is an instance the docker command line client. @@ -75,6 +77,11 @@ func (cli *DockerCli) Err() io.Writer { return cli.err } +// SetIn sets the reader used for stdin +func (cli *DockerCli) SetIn(in *InStream) { + cli.in = in +} + // In returns the reader used for stdin func (cli *DockerCli) In() *InStream { return cli.in diff --git a/cli/command/image/client_test.go b/cli/command/image/client_test.go new file mode 100644 index 0000000000..949b09388b --- /dev/null +++ b/cli/command/image/client_test.go @@ -0,0 +1,116 @@ +package image + +import ( + "io" + "io/ioutil" + "strings" + "time" + + "github.com/docker/cli/client" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/image" + "golang.org/x/net/context" +) + +type fakeClient struct { + client.Client + imageTagFunc func(string, string) error + imageSaveFunc func(images []string) (io.ReadCloser, error) + imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) + imagePushFunc func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) + infoFunc func() (types.Info, error) + imagePullFunc func(ref string, options types.ImagePullOptions) (io.ReadCloser, error) + imagesPruneFunc func(pruneFilter filters.Args) (types.ImagesPruneReport, error) + imageLoadFunc func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) + imageListFunc func(options types.ImageListOptions) ([]types.ImageSummary, error) + imageInspectFunc func(image string) (types.ImageInspect, []byte, error) + imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) + imageHistoryFunc func(image string) ([]image.HistoryResponseItem, error) +} + +func (cli *fakeClient) ImageTag(_ context.Context, image, ref string) error { + if cli.imageTagFunc != nil { + return cli.imageTagFunc(image, ref) + } + return nil +} + +func (cli *fakeClient) ImageSave(_ context.Context, images []string) (io.ReadCloser, error) { + if cli.imageSaveFunc != nil { + return cli.imageSaveFunc(images) + } + return ioutil.NopCloser(strings.NewReader("")), nil +} + +func (cli *fakeClient) ImageRemove(_ context.Context, image string, + options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { + if cli.imageRemoveFunc != nil { + return cli.imageRemoveFunc(image, options) + } + return []types.ImageDeleteResponseItem{}, nil +} + +func (cli *fakeClient) ImagePush(_ context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) { + if cli.imagePushFunc != nil { + return cli.imagePushFunc(ref, options) + } + return ioutil.NopCloser(strings.NewReader("")), nil +} + +func (cli *fakeClient) Info(_ context.Context) (types.Info, error) { + if cli.infoFunc != nil { + return cli.infoFunc() + } + return types.Info{}, nil +} + +func (cli *fakeClient) ImagePull(_ context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) { + if cli.imagePullFunc != nil { + cli.imagePullFunc(ref, options) + } + return ioutil.NopCloser(strings.NewReader("")), nil +} + +func (cli *fakeClient) ImagesPrune(_ context.Context, pruneFilter filters.Args) (types.ImagesPruneReport, error) { + if cli.imagesPruneFunc != nil { + return cli.imagesPruneFunc(pruneFilter) + } + return types.ImagesPruneReport{}, nil +} + +func (cli *fakeClient) ImageLoad(_ context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) { + if cli.imageLoadFunc != nil { + return cli.imageLoadFunc(input, quiet) + } + return types.ImageLoadResponse{}, nil +} + +func (cli *fakeClient) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error) { + if cli.imageListFunc != nil { + return cli.imageListFunc(options) + } + return []types.ImageSummary{{}}, nil +} + +func (cli *fakeClient) ImageInspectWithRaw(_ context.Context, image string) (types.ImageInspect, []byte, error) { + if cli.imageInspectFunc != nil { + return cli.imageInspectFunc(image) + } + return types.ImageInspect{}, nil, nil +} + +func (cli *fakeClient) ImageImport(_ context.Context, source types.ImageImportSource, ref string, + options types.ImageImportOptions) (io.ReadCloser, error) { + if cli.imageImportFunc != nil { + return cli.imageImportFunc(source, ref, options) + } + return ioutil.NopCloser(strings.NewReader("")), nil +} + +func (cli *fakeClient) ImageHistory(_ context.Context, img string) ([]image.HistoryResponseItem, error) { + if cli.imageHistoryFunc != nil { + return cli.imageHistoryFunc(img) + } + return []image.HistoryResponseItem{{ID: img, Created: time.Now().Unix()}}, nil +} diff --git a/cli/command/image/history.go b/cli/command/image/history.go index f4a7009f7c..27782d107a 100644 --- a/cli/command/image/history.go +++ b/cli/command/image/history.go @@ -19,7 +19,7 @@ type historyOptions struct { } // NewHistoryCommand creates a new `docker history` command -func NewHistoryCommand(dockerCli *command.DockerCli) *cobra.Command { +func NewHistoryCommand(dockerCli command.Cli) *cobra.Command { var opts historyOptions cmd := &cobra.Command{ @@ -42,7 +42,7 @@ func NewHistoryCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runHistory(dockerCli *command.DockerCli, opts historyOptions) error { +func runHistory(dockerCli command.Cli, opts historyOptions) error { ctx := context.Background() history, err := dockerCli.Client().ImageHistory(ctx, opts.image) diff --git a/cli/command/image/history_test.go b/cli/command/image/history_test.go new file mode 100644 index 0000000000..71605e4fc8 --- /dev/null +++ b/cli/command/image/history_test.go @@ -0,0 +1,108 @@ +package image + +import ( + "bytes" + "fmt" + "io/ioutil" + "regexp" + "testing" + "time" + + "github.com/docker/cli/cli/internal/test" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/pkg/testutil" + "github.com/docker/docker/pkg/testutil/golden" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestNewHistoryCommandErrors(t *testing.T) { + testCases := []struct { + name string + args []string + expectedError string + imageHistoryFunc func(img string) ([]image.HistoryResponseItem, error) + }{ + { + name: "wrong-args", + args: []string{}, + expectedError: "requires exactly 1 argument(s).", + }, + { + name: "client-error", + args: []string{"image:tag"}, + expectedError: "something went wrong", + imageHistoryFunc: func(img string) ([]image.HistoryResponseItem, error) { + return []image.HistoryResponseItem{{}}, errors.Errorf("something went wrong") + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}, buf)) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNewHistoryCommandSuccess(t *testing.T) { + testCases := []struct { + name string + args []string + outputRegex string + imageHistoryFunc func(img string) ([]image.HistoryResponseItem, error) + }{ + { + name: "simple", + args: []string{"image:tag"}, + imageHistoryFunc: func(img string) ([]image.HistoryResponseItem, error) { + return []image.HistoryResponseItem{{ + ID: "1234567890123456789", + Created: time.Now().Unix(), + }}, nil + }, + }, + { + name: "quiet", + args: []string{"--quiet", "image:tag"}, + }, + // TODO: This test is failing since the output does not contain an RFC3339 date + //{ + // name: "non-human", + // args: []string{"--human=false", "image:tag"}, + // outputRegex: "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", // RFC3339 date format match + //}, + { + name: "non-human-header", + args: []string{"--human=false", "image:tag"}, + outputRegex: "CREATED\\sAT", + }, + { + name: "quiet-no-trunc", + args: []string{"--quiet", "--no-trunc", "image:tag"}, + imageHistoryFunc: func(img string) ([]image.HistoryResponseItem, error) { + return []image.HistoryResponseItem{{ + ID: "1234567890123456789", + Created: time.Now().Unix(), + }}, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}, buf)) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + err := cmd.Execute() + assert.NoError(t, err) + actual := buf.String() + if tc.outputRegex == "" { + expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("history-command-success.%s.golden", tc.name))[:]) + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) + } else { + match, _ := regexp.MatchString(tc.outputRegex, actual) + assert.Equal(t, match, true) + } + } +} diff --git a/cli/command/image/import.go b/cli/command/image/import.go index 5284b66e26..4748122273 100644 --- a/cli/command/image/import.go +++ b/cli/command/image/import.go @@ -23,7 +23,7 @@ type importOptions struct { } // NewImportCommand creates a new `docker import` command -func NewImportCommand(dockerCli *command.DockerCli) *cobra.Command { +func NewImportCommand(dockerCli command.Cli) *cobra.Command { var opts importOptions cmd := &cobra.Command{ @@ -48,7 +48,7 @@ func NewImportCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runImport(dockerCli *command.DockerCli, opts importOptions) error { +func runImport(dockerCli command.Cli, opts importOptions) error { var ( in io.Reader srcName = opts.source diff --git a/cli/command/image/import_test.go b/cli/command/image/import_test.go new file mode 100644 index 0000000000..8e0facf168 --- /dev/null +++ b/cli/command/image/import_test.go @@ -0,0 +1,100 @@ +package image + +import ( + "bytes" + "io" + "io/ioutil" + "strings" + "testing" + + "github.com/docker/cli/cli/internal/test" + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/testutil" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestNewImportCommandErrors(t *testing.T) { + testCases := []struct { + name string + args []string + expectedError string + imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) + }{ + { + name: "wrong-args", + args: []string{}, + expectedError: "requires at least 1 argument(s).", + }, + { + name: "import-failed", + args: []string{"testdata/import-command-success.input.txt"}, + expectedError: "something went wrong", + imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) { + return nil, errors.Errorf("something went wrong") + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}, buf)) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNewImportCommandInvalidFile(t *testing.T) { + cmd := NewImportCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer))) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs([]string{"testdata/import-command-success.unexistent-file"}) + testutil.ErrorContains(t, cmd.Execute(), "testdata/import-command-success.unexistent-file") +} + +func TestNewImportCommandSuccess(t *testing.T) { + testCases := []struct { + name string + args []string + imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) + }{ + { + name: "simple", + args: []string{"testdata/import-command-success.input.txt"}, + }, + { + name: "terminal-source", + args: []string{"-"}, + }, + { + name: "double", + args: []string{"-", "image:local"}, + imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) { + assert.Equal(t, ref, "image:local") + return ioutil.NopCloser(strings.NewReader("")), nil + }, + }, + { + name: "message", + args: []string{"--message", "test message", "-"}, + imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) { + assert.Equal(t, options.Message, "test message") + return ioutil.NopCloser(strings.NewReader("")), nil + }, + }, + { + name: "change", + args: []string{"--change", "ENV DEBUG true", "-"}, + imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) { + assert.Equal(t, options.Changes[0], "ENV DEBUG true") + return ioutil.NopCloser(strings.NewReader("")), nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}, buf)) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + assert.NoError(t, cmd.Execute()) + } +} diff --git a/cli/command/image/inspect.go b/cli/command/image/inspect.go index 5d805fcfd5..a510e30764 100644 --- a/cli/command/image/inspect.go +++ b/cli/command/image/inspect.go @@ -15,7 +15,7 @@ type inspectOptions struct { } // newInspectCommand creates a new cobra.Command for `docker image inspect` -func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { +func newInspectCommand(dockerCli command.Cli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ @@ -33,7 +33,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/cli/command/image/inspect_test.go b/cli/command/image/inspect_test.go new file mode 100644 index 0000000000..ffe77dd26b --- /dev/null +++ b/cli/command/image/inspect_test.go @@ -0,0 +1,92 @@ +package image + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/cli/cli/internal/test" + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/testutil" + "github.com/docker/docker/pkg/testutil/golden" + "github.com/stretchr/testify/assert" +) + +func TestNewInspectCommandErrors(t *testing.T) { + testCases := []struct { + name string + args []string + expectedError string + }{ + { + name: "wrong-args", + args: []string{}, + expectedError: "requires at least 1 argument(s).", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newInspectCommand(test.NewFakeCli(&fakeClient{}, buf)) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNewInspectCommandSuccess(t *testing.T) { + imageInspectInvocationCount := 0 + testCases := []struct { + name string + args []string + imageCount int + imageInspectFunc func(image string) (types.ImageInspect, []byte, error) + }{ + { + name: "simple", + args: []string{"image"}, + imageCount: 1, + imageInspectFunc: func(image string) (types.ImageInspect, []byte, error) { + imageInspectInvocationCount++ + assert.Equal(t, image, "image") + return types.ImageInspect{}, nil, nil + }, + }, + { + name: "format", + imageCount: 1, + args: []string{"--format='{{.ID}}'", "image"}, + imageInspectFunc: func(image string) (types.ImageInspect, []byte, error) { + imageInspectInvocationCount++ + return types.ImageInspect{ID: image}, nil, nil + }, + }, + { + name: "simple-many", + args: []string{"image1", "image2"}, + imageCount: 2, + imageInspectFunc: func(image string) (types.ImageInspect, []byte, error) { + imageInspectInvocationCount++ + if imageInspectInvocationCount == 1 { + assert.Equal(t, image, "image1") + } else { + assert.Equal(t, image, "image2") + } + return types.ImageInspect{}, nil, nil + }, + }, + } + for _, tc := range testCases { + imageInspectInvocationCount = 0 + buf := new(bytes.Buffer) + cmd := newInspectCommand(test.NewFakeCli(&fakeClient{imageInspectFunc: tc.imageInspectFunc}, buf)) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + err := cmd.Execute() + assert.NoError(t, err) + actual := buf.String() + expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("inspect-command-success.%s.golden", tc.name))[:]) + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) + assert.Equal(t, tc.imageCount, imageInspectInvocationCount) + } +} diff --git a/cli/command/image/list.go b/cli/command/image/list.go index 86364489eb..93edaa91e2 100644 --- a/cli/command/image/list.go +++ b/cli/command/image/list.go @@ -23,7 +23,7 @@ type imagesOptions struct { } // NewImagesCommand creates a new `docker images` command -func NewImagesCommand(dockerCli *command.DockerCli) *cobra.Command { +func NewImagesCommand(dockerCli command.Cli) *cobra.Command { opts := imagesOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -50,14 +50,14 @@ func NewImagesCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { +func newListCommand(dockerCli command.Cli) *cobra.Command { cmd := *NewImagesCommand(dockerCli) cmd.Aliases = []string{"images", "list"} cmd.Use = "ls [OPTIONS] [REPOSITORY[:TAG]]" return &cmd } -func runImages(dockerCli *command.DockerCli, opts imagesOptions) error { +func runImages(dockerCli command.Cli, opts imagesOptions) error { ctx := context.Background() filters := opts.filter.Value() diff --git a/cli/command/image/list_test.go b/cli/command/image/list_test.go new file mode 100644 index 0000000000..8b1ba374b6 --- /dev/null +++ b/cli/command/image/list_test.go @@ -0,0 +1,102 @@ +package image + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/internal/test" + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/testutil" + "github.com/docker/docker/pkg/testutil/golden" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestNewImagesCommandErrors(t *testing.T) { + testCases := []struct { + name string + args []string + expectedError string + imageListFunc func(options types.ImageListOptions) ([]types.ImageSummary, error) + }{ + { + name: "wrong-args", + args: []string{"arg1", "arg2"}, + expectedError: "requires at most 1 argument(s).", + }, + { + name: "failed-list", + expectedError: "something went wrong", + imageListFunc: func(options types.ImageListOptions) ([]types.ImageSummary, error) { + return []types.ImageSummary{{}}, errors.Errorf("something went wrong") + }, + }, + } + for _, tc := range testCases { + cmd := NewImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}, new(bytes.Buffer))) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNewImagesCommandSuccess(t *testing.T) { + testCases := []struct { + name string + args []string + imageFormat string + imageListFunc func(options types.ImageListOptions) ([]types.ImageSummary, error) + }{ + { + name: "simple", + }, + { + name: "format", + imageFormat: "raw", + }, + { + name: "quiet-format", + args: []string{"-q"}, + imageFormat: "table", + }, + { + name: "match-name", + args: []string{"image"}, + imageListFunc: func(options types.ImageListOptions) ([]types.ImageSummary, error) { + assert.Equal(t, options.Filters.Get("reference")[0], "image") + return []types.ImageSummary{{}}, nil + }, + }, + { + name: "filters", + args: []string{"--filter", "name=value"}, + imageListFunc: func(options types.ImageListOptions) ([]types.ImageSummary, error) { + assert.Equal(t, options.Filters.Get("name")[0], "value") + return []types.ImageSummary{{}}, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}, buf) + cli.SetConfigfile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat}) + cmd := NewImagesCommand(cli) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + err := cmd.Execute() + assert.NoError(t, err) + actual := buf.String() + expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("list-command-success.%s.golden", tc.name))[:]) + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) + } +} + +func TestNewListCommandAlias(t *testing.T) { + cmd := newListCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer))) + assert.Equal(t, cmd.HasAlias("images"), true) + assert.Equal(t, cmd.HasAlias("list"), true) + assert.Equal(t, cmd.HasAlias("other"), false) +} diff --git a/cli/command/image/load.go b/cli/command/image/load.go index f4b6b4490e..6708599fd7 100644 --- a/cli/command/image/load.go +++ b/cli/command/image/load.go @@ -19,7 +19,7 @@ type loadOptions struct { } // NewLoadCommand creates a new `docker load` command -func NewLoadCommand(dockerCli *command.DockerCli) *cobra.Command { +func NewLoadCommand(dockerCli command.Cli) *cobra.Command { var opts loadOptions cmd := &cobra.Command{ @@ -39,7 +39,7 @@ func NewLoadCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runLoad(dockerCli *command.DockerCli, opts loadOptions) error { +func runLoad(dockerCli command.Cli, opts loadOptions) error { var input io.Reader = dockerCli.In() if opts.input != "" { diff --git a/cli/command/image/load_test.go b/cli/command/image/load_test.go new file mode 100644 index 0000000000..3434d150d4 --- /dev/null +++ b/cli/command/image/load_test.go @@ -0,0 +1,106 @@ +package image + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "strings" + "testing" + + "github.com/docker/cli/cli/internal/test" + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/testutil" + "github.com/docker/docker/pkg/testutil/golden" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestNewLoadCommandErrors(t *testing.T) { + testCases := []struct { + name string + args []string + isTerminalIn bool + expectedError string + imageLoadFunc func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) + }{ + { + name: "wrong-args", + args: []string{"arg"}, + expectedError: "accepts no argument(s).", + }, + { + name: "input-to-terminal", + isTerminalIn: true, + expectedError: "requested load from stdin, but stdin is empty", + }, + { + name: "pull-error", + expectedError: "something went wrong", + imageLoadFunc: func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) { + return types.ImageLoadResponse{}, errors.Errorf("something went wrong") + }, + }, + } + for _, tc := range testCases { + cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}, new(bytes.Buffer)) + cli.In().SetIsTerminal(tc.isTerminalIn) + cmd := NewLoadCommand(cli) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNewLoadCommandInvalidInput(t *testing.T) { + expectedError := "open *" + cmd := NewLoadCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer))) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs([]string{"--input", "*"}) + err := cmd.Execute() + assert.NotNil(t, err) + assert.Contains(t, err.Error(), expectedError) +} + +func TestNewLoadCommandSuccess(t *testing.T) { + testCases := []struct { + name string + args []string + imageLoadFunc func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) + }{ + { + name: "simple", + imageLoadFunc: func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) { + return types.ImageLoadResponse{Body: ioutil.NopCloser(strings.NewReader("Success"))}, nil + }, + }, + { + name: "json", + imageLoadFunc: func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) { + json := "{\"ID\": \"1\"}" + return types.ImageLoadResponse{ + Body: ioutil.NopCloser(strings.NewReader(json)), + JSON: true, + }, nil + }, + }, + { + name: "input-file", + args: []string{"--input", "testdata/load-command-success.input.txt"}, + imageLoadFunc: func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) { + return types.ImageLoadResponse{Body: ioutil.NopCloser(strings.NewReader("Success"))}, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := NewLoadCommand(test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}, buf)) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + err := cmd.Execute() + assert.NoError(t, err) + actual := buf.String() + expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("load-command-success.%s.golden", tc.name))[:]) + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) + } +} diff --git a/cli/command/image/prune.go b/cli/command/image/prune.go index a60ce2080a..3d67b0939e 100644 --- a/cli/command/image/prune.go +++ b/cli/command/image/prune.go @@ -19,7 +19,7 @@ type pruneOptions struct { } // NewPruneCommand returns a new cobra prune command for images -func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { +func NewPruneCommand(dockerCli command.Cli) *cobra.Command { opts := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -55,7 +55,7 @@ Are you sure you want to continue?` 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) { pruneFilters := opts.filter.Value() pruneFilters.Add("dangling", fmt.Sprintf("%v", !opts.all)) pruneFilters = command.PruneFilters(dockerCli, pruneFilters) @@ -90,6 +90,6 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u // RunPrune calls the Image Prune API // This returns the amount of space reclaimed and a detailed output string -func RunPrune(dockerCli *command.DockerCli, all bool, filter opts.FilterOpt) (uint64, string, error) { +func RunPrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) { return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter}) } diff --git a/cli/command/image/prune_test.go b/cli/command/image/prune_test.go new file mode 100644 index 0000000000..f118158cdf --- /dev/null +++ b/cli/command/image/prune_test.go @@ -0,0 +1,100 @@ +package image + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/cli/cli/internal/test" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/pkg/testutil" + "github.com/docker/docker/pkg/testutil/golden" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestNewPruneCommandErrors(t *testing.T) { + testCases := []struct { + name string + args []string + expectedError string + imagesPruneFunc func(pruneFilter filters.Args) (types.ImagesPruneReport, error) + }{ + { + name: "wrong-args", + args: []string{"something"}, + expectedError: "accepts no argument(s).", + }, + { + name: "prune-error", + args: []string{"--force"}, + expectedError: "something went wrong", + imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) { + return types.ImagesPruneReport{}, errors.Errorf("something went wrong") + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{ + imagesPruneFunc: tc.imagesPruneFunc, + }, buf)) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNewPruneCommandSuccess(t *testing.T) { + testCases := []struct { + name string + args []string + imagesPruneFunc func(pruneFilter filters.Args) (types.ImagesPruneReport, error) + }{ + { + name: "all", + args: []string{"--all"}, + imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) { + assert.Equal(t, pruneFilter.Get("dangling")[0], "false") + return types.ImagesPruneReport{}, nil + }, + }, + { + name: "force-deleted", + args: []string{"--force"}, + imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) { + assert.Equal(t, pruneFilter.Get("dangling")[0], "true") + return types.ImagesPruneReport{ + ImagesDeleted: []types.ImageDeleteResponseItem{{Deleted: "image1"}}, + SpaceReclaimed: 1, + }, nil + }, + }, + { + name: "force-untagged", + args: []string{"--force"}, + imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) { + assert.Equal(t, pruneFilter.Get("dangling")[0], "true") + return types.ImagesPruneReport{ + ImagesDeleted: []types.ImageDeleteResponseItem{{Untagged: "image1"}}, + SpaceReclaimed: 2, + }, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{ + imagesPruneFunc: tc.imagesPruneFunc, + }, buf)) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + err := cmd.Execute() + assert.NoError(t, err) + actual := buf.String() + expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("prune-command-success.%s.golden", tc.name))[:]) + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) + } +} diff --git a/cli/command/image/pull.go b/cli/command/image/pull.go index c4be52fe8a..e60e5a4348 100644 --- a/cli/command/image/pull.go +++ b/cli/command/image/pull.go @@ -19,7 +19,7 @@ type pullOptions struct { } // NewPullCommand creates a new `docker pull` command -func NewPullCommand(dockerCli *command.DockerCli) *cobra.Command { +func NewPullCommand(dockerCli command.Cli) *cobra.Command { var opts pullOptions cmd := &cobra.Command{ @@ -40,7 +40,7 @@ func NewPullCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runPull(dockerCli *command.DockerCli, opts pullOptions) error { +func runPull(dockerCli command.Cli, opts pullOptions) error { distributionRef, err := reference.ParseNormalizedNamed(opts.remote) if err != nil { return err diff --git a/cli/command/image/pull_test.go b/cli/command/image/pull_test.go new file mode 100644 index 0000000000..c51a8746fc --- /dev/null +++ b/cli/command/image/pull_test.go @@ -0,0 +1,85 @@ +package image + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/internal/test" + "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/testutil" + "github.com/docker/docker/pkg/testutil/golden" + "github.com/docker/docker/registry" + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" +) + +func TestNewPullCommandErrors(t *testing.T) { + testCases := []struct { + name string + args []string + expectedError string + trustedPullFunc func(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, + authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error + }{ + { + name: "wrong-args", + expectedError: "requires exactly 1 argument(s).", + args: []string{}, + }, + { + name: "invalid-name", + expectedError: "invalid reference format: repository name must be lowercase", + args: []string{"UPPERCASE_REPO"}, + }, + { + name: "all-tags-with-tag", + expectedError: "tag can't be used with --all-tags/-a", + args: []string{"--all-tags", "image:tag"}, + }, + { + name: "pull-error", + args: []string{"--disable-content-trust=false", "image:tag"}, + expectedError: "you are not authorized to perform this operation: server returned 401.", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := NewPullCommand(test.NewFakeCli(&fakeClient{}, buf)) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNewPullCommandSuccess(t *testing.T) { + testCases := []struct { + name string + args []string + trustedPullFunc func(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, + authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error + }{ + { + name: "simple", + args: []string{"image:tag"}, + }, + { + name: "simple-no-tag", + args: []string{"image"}, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := NewPullCommand(test.NewFakeCli(&fakeClient{}, buf)) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + err := cmd.Execute() + assert.NoError(t, err) + actual := buf.String() + expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("pull-command-success.%s.golden", tc.name))[:]) + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) + } +} diff --git a/cli/command/image/push.go b/cli/command/image/push.go index 00b3d96f04..cc95897bd0 100644 --- a/cli/command/image/push.go +++ b/cli/command/image/push.go @@ -12,7 +12,7 @@ import ( ) // NewPushCommand creates a new `docker push` command -func NewPushCommand(dockerCli *command.DockerCli) *cobra.Command { +func NewPushCommand(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "push [OPTIONS] NAME[:TAG]", Short: "Push an image or a repository to a registry", @@ -29,7 +29,7 @@ func NewPushCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runPush(dockerCli *command.DockerCli, remote string) error { +func runPush(dockerCli command.Cli, remote string) error { ref, err := reference.ParseNormalizedNamed(remote) if err != nil { return err diff --git a/cli/command/image/push_test.go b/cli/command/image/push_test.go new file mode 100644 index 0000000000..d34941bd42 --- /dev/null +++ b/cli/command/image/push_test.go @@ -0,0 +1,84 @@ +package image + +import ( + "bytes" + "io" + "io/ioutil" + "strings" + "testing" + + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/internal/test" + "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types" + "github.com/docker/docker/registry" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" +) + +func TestNewPushCommandErrors(t *testing.T) { + testCases := []struct { + name string + args []string + expectedError string + imagePushFunc func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) + }{ + { + name: "wrong-args", + args: []string{}, + expectedError: "requires exactly 1 argument(s).", + }, + { + name: "invalid-name", + args: []string{"UPPERCASE_REPO"}, + expectedError: "invalid reference format: repository name must be lowercase", + }, + { + name: "push-failed", + args: []string{"image:repo"}, + expectedError: "Failed to push", + imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) { + return ioutil.NopCloser(strings.NewReader("")), errors.Errorf("Failed to push") + }, + }, + { + name: "trust-error", + args: []string{"--disable-content-trust=false", "image:repo"}, + expectedError: "you are not authorized to perform this operation: server returned 401.", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := NewPushCommand(test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc}, buf)) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNewPushCommandSuccess(t *testing.T) { + testCases := []struct { + name string + args []string + trustedPushFunc func(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, + ref reference.Named, authConfig types.AuthConfig, + requestPrivilege types.RequestPrivilegeFunc) error + }{ + { + name: "simple", + args: []string{"image:tag"}, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := NewPushCommand(test.NewFakeCli(&fakeClient{ + imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) { + return ioutil.NopCloser(strings.NewReader("")), nil + }, + }, buf)) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + assert.NoError(t, cmd.Execute()) + } +} diff --git a/cli/command/image/remove.go b/cli/command/image/remove.go index 0948bb7bef..91bf2f8786 100644 --- a/cli/command/image/remove.go +++ b/cli/command/image/remove.go @@ -19,7 +19,7 @@ type removeOptions struct { } // NewRemoveCommand creates a new `docker remove` command -func NewRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { +func NewRemoveCommand(dockerCli command.Cli) *cobra.Command { var opts removeOptions cmd := &cobra.Command{ @@ -39,14 +39,14 @@ func NewRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { +func newRemoveCommand(dockerCli command.Cli) *cobra.Command { cmd := *NewRemoveCommand(dockerCli) cmd.Aliases = []string{"rmi", "remove"} cmd.Use = "rm [OPTIONS] IMAGE [IMAGE...]" return &cmd } -func runRemove(dockerCli *command.DockerCli, opts removeOptions, images []string) error { +func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error { client := dockerCli.Client() ctx := context.Background() diff --git a/cli/command/image/remove_test.go b/cli/command/image/remove_test.go new file mode 100644 index 0000000000..9b6508d4ad --- /dev/null +++ b/cli/command/image/remove_test.go @@ -0,0 +1,103 @@ +package image + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/cli/cli/internal/test" + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/testutil" + "github.com/docker/docker/pkg/testutil/golden" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestNewRemoveCommandAlias(t *testing.T) { + cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer))) + assert.Equal(t, cmd.HasAlias("rmi"), true) + assert.Equal(t, cmd.HasAlias("remove"), true) + assert.Equal(t, cmd.HasAlias("other"), false) +} + +func TestNewRemoveCommandErrors(t *testing.T) { + testCases := []struct { + name string + args []string + expectedError string + imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) + }{ + { + name: "wrong args", + expectedError: "requires at least 1 argument(s).", + }, + { + name: "ImageRemove fail", + args: []string{"arg1"}, + expectedError: "error removing image", + imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { + assert.Equal(t, options.Force, false) + assert.Equal(t, options.PruneChildren, true) + return []types.ImageDeleteResponseItem{}, errors.Errorf("error removing image") + }, + }, + } + for _, tc := range testCases { + cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{ + imageRemoveFunc: tc.imageRemoveFunc, + }, new(bytes.Buffer))) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNewRemoveCommandSuccess(t *testing.T) { + testCases := []struct { + name string + args []string + imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) + }{ + { + name: "Image Deleted", + args: []string{"image1"}, + imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { + assert.Equal(t, image, "image1") + return []types.ImageDeleteResponseItem{{Deleted: image}}, nil + }, + }, + { + name: "Image Untagged", + args: []string{"image1"}, + imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { + assert.Equal(t, image, "image1") + return []types.ImageDeleteResponseItem{{Untagged: image}}, nil + }, + }, + { + name: "Image Deleted and Untagged", + args: []string{"image1", "image2"}, + imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { + if image == "image1" { + return []types.ImageDeleteResponseItem{{Untagged: image}}, nil + } + return []types.ImageDeleteResponseItem{{Deleted: image}}, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{ + imageRemoveFunc: tc.imageRemoveFunc, + }, buf)) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + assert.NoError(t, cmd.Execute()) + err := cmd.Execute() + assert.NoError(t, err) + actual := buf.String() + expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("remove-command-success.%s.golden", tc.name))[:]) + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) + } +} diff --git a/cli/command/image/save.go b/cli/command/image/save.go index cb049d5eaf..ba666d2740 100644 --- a/cli/command/image/save.go +++ b/cli/command/image/save.go @@ -16,7 +16,7 @@ type saveOptions struct { } // NewSaveCommand creates a new `docker save` command -func NewSaveCommand(dockerCli *command.DockerCli) *cobra.Command { +func NewSaveCommand(dockerCli command.Cli) *cobra.Command { var opts saveOptions cmd := &cobra.Command{ @@ -36,7 +36,7 @@ func NewSaveCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runSave(dockerCli *command.DockerCli, opts saveOptions) error { +func runSave(dockerCli command.Cli, opts saveOptions) error { if opts.output == "" && dockerCli.Out().IsTerminal() { return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect") } diff --git a/cli/command/image/save_test.go b/cli/command/image/save_test.go new file mode 100644 index 0000000000..fe8a04bf6f --- /dev/null +++ b/cli/command/image/save_test.go @@ -0,0 +1,98 @@ +package image + +import ( + "bytes" + "io" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/docker/cli/cli/internal/test" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestNewSaveCommandErrors(t *testing.T) { + testCases := []struct { + name string + args []string + isTerminal bool + expectedError string + imageSaveFunc func(images []string) (io.ReadCloser, error) + }{ + { + name: "wrong args", + args: []string{}, + expectedError: "requires at least 1 argument(s).", + }, + { + name: "output to terminal", + args: []string{"output", "file", "arg1"}, + isTerminal: true, + expectedError: "Cowardly refusing to save to a terminal. Use the -o flag or redirect.", + }, + { + name: "ImageSave fail", + args: []string{"arg1"}, + isTerminal: false, + expectedError: "error saving image", + imageSaveFunc: func(images []string) (io.ReadCloser, error) { + return ioutil.NopCloser(strings.NewReader("")), errors.Errorf("error saving image") + }, + }, + } + for _, tc := range testCases { + cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc}, new(bytes.Buffer)) + cli.Out().SetIsTerminal(tc.isTerminal) + cmd := NewSaveCommand(cli) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNewSaveCommandSuccess(t *testing.T) { + testCases := []struct { + args []string + isTerminal bool + imageSaveFunc func(images []string) (io.ReadCloser, error) + deferredFunc func() + }{ + { + args: []string{"-o", "save_tmp_file", "arg1"}, + isTerminal: true, + imageSaveFunc: func(images []string) (io.ReadCloser, error) { + assert.Equal(t, len(images), 1) + assert.Equal(t, images[0], "arg1") + return ioutil.NopCloser(strings.NewReader("")), nil + }, + deferredFunc: func() { + os.Remove("save_tmp_file") + }, + }, + { + args: []string{"arg1", "arg2"}, + isTerminal: false, + imageSaveFunc: func(images []string) (io.ReadCloser, error) { + assert.Equal(t, len(images), 2) + assert.Equal(t, images[0], "arg1") + assert.Equal(t, images[1], "arg2") + return ioutil.NopCloser(strings.NewReader("")), nil + }, + }, + } + for _, tc := range testCases { + cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{ + imageSaveFunc: func(images []string) (io.ReadCloser, error) { + return ioutil.NopCloser(strings.NewReader("")), nil + }, + }, new(bytes.Buffer))) + cmd.SetOutput(ioutil.Discard) + cmd.SetArgs(tc.args) + assert.NoError(t, cmd.Execute()) + if tc.deferredFunc != nil { + tc.deferredFunc() + } + } +} diff --git a/cli/command/image/tag.go b/cli/command/image/tag.go index ab8dc72498..2a50c127c4 100644 --- a/cli/command/image/tag.go +++ b/cli/command/image/tag.go @@ -14,7 +14,7 @@ type tagOptions struct { } // NewTagCommand creates a new `docker tag` command -func NewTagCommand(dockerCli *command.DockerCli) *cobra.Command { +func NewTagCommand(dockerCli command.Cli) *cobra.Command { var opts tagOptions cmd := &cobra.Command{ @@ -34,7 +34,7 @@ func NewTagCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runTag(dockerCli *command.DockerCli, opts tagOptions) error { +func runTag(dockerCli command.Cli, opts tagOptions) error { ctx := context.Background() return dockerCli.Client().ImageTag(ctx, opts.image, opts.name) diff --git a/cli/command/image/tag_test.go b/cli/command/image/tag_test.go new file mode 100644 index 0000000000..40f5b46b32 --- /dev/null +++ b/cli/command/image/tag_test.go @@ -0,0 +1,43 @@ +package image + +import ( + "bytes" + "io/ioutil" + "testing" + + "github.com/docker/cli/cli/internal/test" + "github.com/stretchr/testify/assert" +) + +func TestCliNewTagCommandErrors(t *testing.T) { + testCases := [][]string{ + {}, + {"image1"}, + {"image1", "image2", "image3"}, + } + expectedError := "\"tag\" requires exactly 2 argument(s)." + buf := new(bytes.Buffer) + for _, args := range testCases { + cmd := NewTagCommand(test.NewFakeCli(&fakeClient{}, buf)) + cmd.SetArgs(args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), expectedError) + } +} + +func TestCliNewTagCommand(t *testing.T) { + buf := new(bytes.Buffer) + cmd := NewTagCommand( + test.NewFakeCli(&fakeClient{ + imageTagFunc: func(image string, ref string) error { + assert.Equal(t, image, "image1") + assert.Equal(t, ref, "image2") + return nil + }, + }, buf)) + cmd.SetArgs([]string{"image1", "image2"}) + cmd.SetOutput(ioutil.Discard) + assert.NoError(t, cmd.Execute()) + value, _ := cmd.Flags().GetBool("interspersed") + assert.Equal(t, value, false) +} diff --git a/cli/command/image/testdata/history-command-success.quiet-no-trunc.golden b/cli/command/image/testdata/history-command-success.quiet-no-trunc.golden new file mode 100644 index 0000000000..65103f6354 --- /dev/null +++ b/cli/command/image/testdata/history-command-success.quiet-no-trunc.golden @@ -0,0 +1 @@ +1234567890123456789 diff --git a/cli/command/image/testdata/history-command-success.quiet.golden b/cli/command/image/testdata/history-command-success.quiet.golden new file mode 100644 index 0000000000..42c7c82cc8 --- /dev/null +++ b/cli/command/image/testdata/history-command-success.quiet.golden @@ -0,0 +1 @@ +tag diff --git a/cli/command/image/testdata/history-command-success.simple.golden b/cli/command/image/testdata/history-command-success.simple.golden new file mode 100644 index 0000000000..8aa590526f --- /dev/null +++ b/cli/command/image/testdata/history-command-success.simple.golden @@ -0,0 +1,2 @@ +IMAGE CREATED CREATED BY SIZE COMMENT +123456789012 Less than a second ago 0B diff --git a/cli/command/image/testdata/import-command-success.input.txt b/cli/command/image/testdata/import-command-success.input.txt new file mode 100644 index 0000000000..7ab5949b13 --- /dev/null +++ b/cli/command/image/testdata/import-command-success.input.txt @@ -0,0 +1 @@ +file input test \ No newline at end of file diff --git a/cli/command/image/testdata/inspect-command-success.format.golden b/cli/command/image/testdata/inspect-command-success.format.golden new file mode 100644 index 0000000000..f934996b07 --- /dev/null +++ b/cli/command/image/testdata/inspect-command-success.format.golden @@ -0,0 +1 @@ +'image' diff --git a/cli/command/image/testdata/inspect-command-success.simple-many.golden b/cli/command/image/testdata/inspect-command-success.simple-many.golden new file mode 100644 index 0000000000..d4042589f8 --- /dev/null +++ b/cli/command/image/testdata/inspect-command-success.simple-many.golden @@ -0,0 +1,50 @@ +[ + { + "Id": "", + "RepoTags": null, + "RepoDigests": null, + "Parent": "", + "Comment": "", + "Created": "", + "Container": "", + "ContainerConfig": null, + "DockerVersion": "", + "Author": "", + "Config": null, + "Architecture": "", + "Os": "", + "Size": 0, + "VirtualSize": 0, + "GraphDriver": { + "Data": null, + "Name": "" + }, + "RootFS": { + "Type": "" + } + }, + { + "Id": "", + "RepoTags": null, + "RepoDigests": null, + "Parent": "", + "Comment": "", + "Created": "", + "Container": "", + "ContainerConfig": null, + "DockerVersion": "", + "Author": "", + "Config": null, + "Architecture": "", + "Os": "", + "Size": 0, + "VirtualSize": 0, + "GraphDriver": { + "Data": null, + "Name": "" + }, + "RootFS": { + "Type": "" + } + } +] diff --git a/cli/command/image/testdata/inspect-command-success.simple.golden b/cli/command/image/testdata/inspect-command-success.simple.golden new file mode 100644 index 0000000000..802c52469b --- /dev/null +++ b/cli/command/image/testdata/inspect-command-success.simple.golden @@ -0,0 +1,26 @@ +[ + { + "Id": "", + "RepoTags": null, + "RepoDigests": null, + "Parent": "", + "Comment": "", + "Created": "", + "Container": "", + "ContainerConfig": null, + "DockerVersion": "", + "Author": "", + "Config": null, + "Architecture": "", + "Os": "", + "Size": 0, + "VirtualSize": 0, + "GraphDriver": { + "Data": null, + "Name": "" + }, + "RootFS": { + "Type": "" + } + } +] diff --git a/cli/command/image/testdata/list-command-success.filters.golden b/cli/command/image/testdata/list-command-success.filters.golden new file mode 100644 index 0000000000..e3b8109bcf --- /dev/null +++ b/cli/command/image/testdata/list-command-success.filters.golden @@ -0,0 +1 @@ +REPOSITORY TAG IMAGE ID CREATED SIZE diff --git a/cli/command/image/testdata/list-command-success.format.golden b/cli/command/image/testdata/list-command-success.format.golden new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cli/command/image/testdata/list-command-success.match-name.golden b/cli/command/image/testdata/list-command-success.match-name.golden new file mode 100644 index 0000000000..e3b8109bcf --- /dev/null +++ b/cli/command/image/testdata/list-command-success.match-name.golden @@ -0,0 +1 @@ +REPOSITORY TAG IMAGE ID CREATED SIZE diff --git a/cli/command/image/testdata/list-command-success.quiet-format.golden b/cli/command/image/testdata/list-command-success.quiet-format.golden new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cli/command/image/testdata/list-command-success.simple.golden b/cli/command/image/testdata/list-command-success.simple.golden new file mode 100644 index 0000000000..e3b8109bcf --- /dev/null +++ b/cli/command/image/testdata/list-command-success.simple.golden @@ -0,0 +1 @@ +REPOSITORY TAG IMAGE ID CREATED SIZE diff --git a/cli/command/image/testdata/load-command-success.input-file.golden b/cli/command/image/testdata/load-command-success.input-file.golden new file mode 100644 index 0000000000..51da4200ab --- /dev/null +++ b/cli/command/image/testdata/load-command-success.input-file.golden @@ -0,0 +1 @@ +Success \ No newline at end of file diff --git a/cli/command/image/testdata/load-command-success.input.txt b/cli/command/image/testdata/load-command-success.input.txt new file mode 100644 index 0000000000..7ab5949b13 --- /dev/null +++ b/cli/command/image/testdata/load-command-success.input.txt @@ -0,0 +1 @@ +file input test \ No newline at end of file diff --git a/cli/command/image/testdata/load-command-success.json.golden b/cli/command/image/testdata/load-command-success.json.golden new file mode 100644 index 0000000000..c17f16ecd7 --- /dev/null +++ b/cli/command/image/testdata/load-command-success.json.golden @@ -0,0 +1 @@ +1: diff --git a/cli/command/image/testdata/load-command-success.simple.golden b/cli/command/image/testdata/load-command-success.simple.golden new file mode 100644 index 0000000000..51da4200ab --- /dev/null +++ b/cli/command/image/testdata/load-command-success.simple.golden @@ -0,0 +1 @@ +Success \ No newline at end of file diff --git a/cli/command/image/testdata/prune-command-success.all.golden b/cli/command/image/testdata/prune-command-success.all.golden new file mode 100644 index 0000000000..4d1445280c --- /dev/null +++ b/cli/command/image/testdata/prune-command-success.all.golden @@ -0,0 +1,2 @@ +WARNING! This will remove all images without at least one container associated to them. +Are you sure you want to continue? [y/N] Total reclaimed space: 0B diff --git a/cli/command/image/testdata/prune-command-success.force-deleted.golden b/cli/command/image/testdata/prune-command-success.force-deleted.golden new file mode 100644 index 0000000000..1b6efd4a99 --- /dev/null +++ b/cli/command/image/testdata/prune-command-success.force-deleted.golden @@ -0,0 +1,4 @@ +Deleted Images: +deleted: image1 + +Total reclaimed space: 1B diff --git a/cli/command/image/testdata/prune-command-success.force-untagged.golden b/cli/command/image/testdata/prune-command-success.force-untagged.golden new file mode 100644 index 0000000000..725468fe56 --- /dev/null +++ b/cli/command/image/testdata/prune-command-success.force-untagged.golden @@ -0,0 +1,4 @@ +Deleted Images: +untagged: image1 + +Total reclaimed space: 2B diff --git a/cli/command/image/testdata/pull-command-success.simple-no-tag.golden b/cli/command/image/testdata/pull-command-success.simple-no-tag.golden new file mode 100644 index 0000000000..946de409a4 --- /dev/null +++ b/cli/command/image/testdata/pull-command-success.simple-no-tag.golden @@ -0,0 +1 @@ +Using default tag: latest diff --git a/cli/command/image/testdata/pull-command-success.simple.golden b/cli/command/image/testdata/pull-command-success.simple.golden new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden b/cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden new file mode 100644 index 0000000000..4efc53719d --- /dev/null +++ b/cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden @@ -0,0 +1,4 @@ +Untagged: image1 +Deleted: image2 +Untagged: image1 +Deleted: image2 diff --git a/cli/command/image/testdata/remove-command-success.Image Deleted.golden b/cli/command/image/testdata/remove-command-success.Image Deleted.golden new file mode 100644 index 0000000000..382724d39f --- /dev/null +++ b/cli/command/image/testdata/remove-command-success.Image Deleted.golden @@ -0,0 +1,2 @@ +Deleted: image1 +Deleted: image1 diff --git a/cli/command/image/testdata/remove-command-success.Image Untagged.golden b/cli/command/image/testdata/remove-command-success.Image Untagged.golden new file mode 100644 index 0000000000..c795dac19f --- /dev/null +++ b/cli/command/image/testdata/remove-command-success.Image Untagged.golden @@ -0,0 +1,2 @@ +Untagged: image1 +Untagged: image1 diff --git a/cli/command/image/trust.go b/cli/command/image/trust.go index 282fcffa3d..c5b50ba460 100644 --- a/cli/command/image/trust.go +++ b/cli/command/image/trust.go @@ -29,7 +29,7 @@ type target struct { } // trustedPush handles content trust pushing of an image -func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { +func trustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { responseBody, err := imagePushPrivileged(ctx, cli, authConfig, ref, requestPrivilege) if err != nil { return err @@ -42,7 +42,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry // PushTrustedReference pushes a canonical reference to the trust server. // nolint: gocyclo -func PushTrustedReference(cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, in io.Reader) error { +func PushTrustedReference(cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, in io.Reader) error { // If it is a trusted push we would like to find the target entry which match the // tag provided in the function and then do an AddTarget later. target := &client.Target{} @@ -203,7 +203,7 @@ func addTargetToAllSignableRoles(repo *client.NotaryRepository, target *client.T } // imagePushPrivileged push the image -func imagePushPrivileged(ctx context.Context, cli *command.DockerCli, authConfig types.AuthConfig, ref reference.Named, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) { +func imagePushPrivileged(ctx context.Context, cli command.Cli, authConfig types.AuthConfig, ref reference.Named, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) { encodedAuth, err := command.EncodeAuthToBase64(authConfig) if err != nil { return nil, err @@ -217,7 +217,7 @@ func imagePushPrivileged(ctx context.Context, cli *command.DockerCli, authConfig } // trustedPull handles content trust pulling of an image -func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { +func trustedPull(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { var refs []target notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") @@ -296,7 +296,7 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry } // imagePullPrivileged pulls the image and displays it to the output -func imagePullPrivileged(ctx context.Context, cli *command.DockerCli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error { +func imagePullPrivileged(ctx context.Context, cli command.Cli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error { encodedAuth, err := command.EncodeAuthToBase64(authConfig) if err != nil { @@ -318,7 +318,7 @@ func imagePullPrivileged(ctx context.Context, cli *command.DockerCli, authConfig } // TrustedReference returns the canonical trusted reference for an image reference -func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged, rs registry.Service) (reference.Canonical, error) { +func TrustedReference(ctx context.Context, cli command.Cli, ref reference.NamedTagged, rs registry.Service) (reference.Canonical, error) { var ( repoInfo *registry.RepositoryInfo err error @@ -372,7 +372,7 @@ func convertTarget(t client.Target) (target, error) { } // TagTrusted tags a trusted ref -func TagTrusted(ctx context.Context, cli *command.DockerCli, trustedRef reference.Canonical, ref reference.NamedTagged) error { +func TagTrusted(ctx context.Context, cli command.Cli, trustedRef reference.Canonical, ref reference.NamedTagged) error { // Use familiar references when interacting with client and output familiarRef := reference.FamiliarString(ref) trustedFamiliarRef := reference.FamiliarString(trustedRef) diff --git a/cli/command/in.go b/cli/command/in.go index 50de77ee9b..815d2a2028 100644 --- a/cli/command/in.go +++ b/cli/command/in.go @@ -1,20 +1,16 @@ package command import ( - "io" - "os" - "runtime" - + "errors" "github.com/docker/docker/pkg/term" - "github.com/pkg/errors" + "io" + "runtime" ) // InStream is an input stream used by the DockerCli to read user input type InStream struct { - in io.ReadCloser - fd uintptr - isTerminal bool - state *term.State + CommonStream + in io.ReadCloser } func (i *InStream) Read(p []byte) (int, error) { @@ -26,32 +22,6 @@ func (i *InStream) Close() error { return i.in.Close() } -// FD returns the file descriptor number for this stream -func (i *InStream) FD() uintptr { - return i.fd -} - -// IsTerminal returns true if this stream is connected to a terminal -func (i *InStream) IsTerminal() bool { - return i.isTerminal -} - -// SetRawTerminal sets raw mode on the input terminal -func (i *InStream) SetRawTerminal() (err error) { - if os.Getenv("NORAW") != "" || !i.isTerminal { - return nil - } - i.state, err = term.SetRawTerminal(i.fd) - return err -} - -// RestoreTerminal restores normal mode to the terminal -func (i *InStream) RestoreTerminal() { - if i.state != nil { - term.RestoreTerminal(i.fd, i.state) - } -} - // CheckTty checks if we are trying to attach to a container tty // from a non-tty client input stream, and if so, returns an error. func (i *InStream) CheckTty(attachStdin, ttyMode bool) error { @@ -71,5 +41,5 @@ func (i *InStream) CheckTty(attachStdin, ttyMode bool) error { // NewInStream returns a new InStream object from a ReadCloser func NewInStream(in io.ReadCloser) *InStream { fd, isTerminal := term.GetFdInfo(in) - return &InStream{in: in, fd: fd, isTerminal: isTerminal} + return &InStream{CommonStream: CommonStream{fd: fd, isTerminal: isTerminal}, in: in} } diff --git a/cli/command/out.go b/cli/command/out.go index 85718d7acd..622e758cc4 100644 --- a/cli/command/out.go +++ b/cli/command/out.go @@ -1,52 +1,22 @@ package command import ( - "io" - "os" - "github.com/Sirupsen/logrus" "github.com/docker/docker/pkg/term" + "io" ) // OutStream is an output stream used by the DockerCli to write normal program // output. type OutStream struct { - out io.Writer - fd uintptr - isTerminal bool - state *term.State + CommonStream + out io.Writer } func (o *OutStream) Write(p []byte) (int, error) { return o.out.Write(p) } -// FD returns the file descriptor number for this stream -func (o *OutStream) FD() uintptr { - return o.fd -} - -// IsTerminal returns true if this stream is connected to a terminal -func (o *OutStream) IsTerminal() bool { - return o.isTerminal -} - -// SetRawTerminal sets raw mode on the output terminal -func (o *OutStream) SetRawTerminal() (err error) { - if os.Getenv("NORAW") != "" || !o.isTerminal { - return nil - } - o.state, err = term.SetRawTerminalOutput(o.fd) - return err -} - -// RestoreTerminal restores normal mode to the terminal -func (o *OutStream) RestoreTerminal() { - if o.state != nil { - term.RestoreTerminal(o.fd, o.state) - } -} - // GetTtySize returns the height and width in characters of the tty func (o *OutStream) GetTtySize() (uint, uint) { if !o.isTerminal { @@ -65,5 +35,5 @@ func (o *OutStream) GetTtySize() (uint, uint) { // NewOutStream returns a new OutStream object from a Writer func NewOutStream(out io.Writer) *OutStream { fd, isTerminal := term.GetFdInfo(out) - return &OutStream{out: out, fd: fd, isTerminal: isTerminal} + return &OutStream{CommonStream: CommonStream{fd: fd, isTerminal: isTerminal}, out: out} } diff --git a/cli/command/registry.go b/cli/command/registry.go index e13bba775d..884fa6ec40 100644 --- a/cli/command/registry.go +++ b/cli/command/registry.go @@ -21,7 +21,7 @@ import ( ) // ElectAuthServer returns the default registry to use (by asking the daemon) -func ElectAuthServer(ctx context.Context, cli *DockerCli) string { +func ElectAuthServer(ctx context.Context, cli Cli) string { // The daemon `/info` endpoint informs us of the default registry being // used. This is essential in cross-platforms environment, where for // example a Linux client might be interacting with a Windows daemon, hence @@ -46,7 +46,7 @@ func EncodeAuthToBase64(authConfig types.AuthConfig) (string, error) { // RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info // for the given command. -func RegistryAuthenticationPrivilegedFunc(cli *DockerCli, index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc { +func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc { return func() (string, error) { fmt.Fprintf(cli.Out(), "\nPlease login prior to %s:\n", cmdName) indexServer := registry.GetAuthConfigKey(index) @@ -62,7 +62,7 @@ func RegistryAuthenticationPrivilegedFunc(cli *DockerCli, index *registrytypes.I // ResolveAuthConfig is like registry.ResolveAuthConfig, but if using the // default index, it uses the default index name for the daemon's platform, // not the client's platform. -func ResolveAuthConfig(ctx context.Context, cli *DockerCli, index *registrytypes.IndexInfo) types.AuthConfig { +func ResolveAuthConfig(ctx context.Context, cli Cli, index *registrytypes.IndexInfo) types.AuthConfig { configKey := index.Name if index.Official { configKey = ElectAuthServer(ctx, cli) @@ -73,10 +73,10 @@ func ResolveAuthConfig(ctx context.Context, cli *DockerCli, index *registrytypes } // ConfigureAuth returns an AuthConfig from the specified user, password and server. -func ConfigureAuth(cli *DockerCli, flUser, flPassword, serverAddress string, isDefaultRegistry bool) (types.AuthConfig, error) { +func ConfigureAuth(cli Cli, flUser, flPassword, serverAddress string, isDefaultRegistry bool) (types.AuthConfig, error) { // On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210 if runtime.GOOS == "windows" { - cli.in = NewInStream(os.Stdin) + cli.SetIn(NewInStream(os.Stdin)) } if !isDefaultRegistry { @@ -160,7 +160,7 @@ func promptWithDefault(out io.Writer, prompt string, configDefault string) { } // RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete image -func RetrieveAuthTokenFromImage(ctx context.Context, cli *DockerCli, image string) (string, error) { +func RetrieveAuthTokenFromImage(ctx context.Context, cli Cli, image string) (string, error) { // Retrieve encoded auth token from the image reference authConfig, err := resolveAuthConfigFromImage(ctx, cli, image) if err != nil { @@ -174,7 +174,7 @@ func RetrieveAuthTokenFromImage(ctx context.Context, cli *DockerCli, image strin } // resolveAuthConfigFromImage retrieves that AuthConfig using the image string -func resolveAuthConfigFromImage(ctx context.Context, cli *DockerCli, image string) (types.AuthConfig, error) { +func resolveAuthConfigFromImage(ctx context.Context, cli Cli, image string) (types.AuthConfig, error) { registryRef, err := reference.ParseNormalizedNamed(image) if err != nil { return types.AuthConfig{}, err diff --git a/cli/command/stream.go b/cli/command/stream.go new file mode 100644 index 0000000000..13a54cc672 --- /dev/null +++ b/cli/command/stream.go @@ -0,0 +1,44 @@ +package command + +import ( + "github.com/docker/docker/pkg/term" + "os" +) + +// CommonStream is an input stream used by the DockerCli to read user input +type CommonStream struct { + fd uintptr + isTerminal bool + state *term.State +} + +// FD returns the file descriptor number for this stream +func (s *CommonStream) FD() uintptr { + return s.fd +} + +// IsTerminal returns true if this stream is connected to a terminal +func (s *CommonStream) IsTerminal() bool { + return s.isTerminal +} + +// SetRawTerminal sets raw mode on the input terminal +func (s *CommonStream) SetRawTerminal() (err error) { + if os.Getenv("NORAW") != "" || !s.isTerminal { + return nil + } + s.state, err = term.SetRawTerminal(s.fd) + return err +} + +// RestoreTerminal restores normal mode to the terminal +func (s *CommonStream) RestoreTerminal() { + if s.state != nil { + term.RestoreTerminal(s.fd, s.state) + } +} + +// SetIsTerminal sets the boolean used for isTerminal +func (s *CommonStream) SetIsTerminal(isTerminal bool) { + s.isTerminal = isTerminal +} diff --git a/cli/command/swarm/unlock_test.go b/cli/command/swarm/unlock_test.go index 0bd433671d..991365f873 100644 --- a/cli/command/swarm/unlock_test.go +++ b/cli/command/swarm/unlock_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/internal/test" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" @@ -96,7 +97,7 @@ func TestSwarmUnlock(t *testing.T) { return nil }, }, buf) - dockerCli.SetIn(ioutil.NopCloser(strings.NewReader(input))) + dockerCli.SetIn(command.NewInStream(ioutil.NopCloser(strings.NewReader(input)))) cmd := newUnlockCommand(dockerCli) assert.NoError(t, cmd.Execute()) } diff --git a/cli/command/volume/prune_test.go b/cli/command/volume/prune_test.go index 2381a41458..33a8d5dc19 100644 --- a/cli/command/volume/prune_test.go +++ b/cli/command/volume/prune_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/internal/test" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -91,7 +92,7 @@ func TestVolumePrunePromptYes(t *testing.T) { volumePruneFunc: simplePruneFunc, }, buf) - cli.SetIn(ioutil.NopCloser(strings.NewReader(input))) + cli.SetIn(command.NewInStream(ioutil.NopCloser(strings.NewReader(input)))) cmd := NewPruneCommand( cli, ) @@ -113,7 +114,7 @@ func TestVolumePrunePromptNo(t *testing.T) { volumePruneFunc: simplePruneFunc, }, buf) - cli.SetIn(ioutil.NopCloser(strings.NewReader(input))) + cli.SetIn(command.NewInStream(ioutil.NopCloser(strings.NewReader(input)))) cmd := NewPruneCommand( cli, ) diff --git a/cli/internal/test/cli.go b/cli/internal/test/cli.go index 081588b0fd..0a0eae3841 100644 --- a/cli/internal/test/cli.go +++ b/cli/internal/test/cli.go @@ -7,6 +7,7 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/config/credentials" "github.com/docker/cli/client" ) @@ -15,23 +16,24 @@ type FakeCli struct { command.DockerCli client client.APIClient configfile *configfile.ConfigFile - out io.Writer + out *command.OutStream err io.Writer - in io.ReadCloser + in *command.InStream + store credentials.Store } // NewFakeCli returns a Cli backed by the fakeCli func NewFakeCli(client client.APIClient, out io.Writer) *FakeCli { return &FakeCli{ client: client, - out: out, + out: command.NewOutStream(out), err: ioutil.Discard, - in: ioutil.NopCloser(strings.NewReader("")), + in: command.NewInStream(ioutil.NopCloser(strings.NewReader(""))), } } // SetIn sets the input of the cli to the specified ReadCloser -func (c *FakeCli) SetIn(in io.ReadCloser) { +func (c *FakeCli) SetIn(in *command.InStream) { c.in = in } @@ -52,7 +54,7 @@ func (c *FakeCli) Client() client.APIClient { // Out returns the output stream (stdout) the cli should write on func (c *FakeCli) Out() *command.OutStream { - return command.NewOutStream(c.out) + return c.out } // Err returns the output stream (stderr) the cli should write on @@ -62,10 +64,18 @@ func (c *FakeCli) Err() io.Writer { // In returns the input stream the cli will use func (c *FakeCli) In() *command.InStream { - return command.NewInStream(c.in) + return c.in } // ConfigFile returns the cli configfile object (to get client configuration) func (c *FakeCli) ConfigFile() *configfile.ConfigFile { return c.configfile } + +// CredentialsStore returns the fake store the cli will use +func (c *FakeCli) CredentialsStore(serverAddress string) credentials.Store { + if c.store == nil { + c.store = NewFakeStore() + } + return c.store +} diff --git a/cli/internal/test/store.go b/cli/internal/test/store.go new file mode 100644 index 0000000000..28e52bab05 --- /dev/null +++ b/cli/internal/test/store.go @@ -0,0 +1,74 @@ +package test + +import ( + "github.com/docker/cli/cli/config/credentials" + "github.com/docker/docker/api/types" +) + +// fake store implements a credentials.Store that only acts as an in memory map +type fakeStore struct { + store map[string]types.AuthConfig + eraseFunc func(serverAddress string) error + getFunc func(serverAddress string) (types.AuthConfig, error) + getAllFunc func() (map[string]types.AuthConfig, error) + storeFunc func(authConfig types.AuthConfig) error +} + +// NewFakeStore creates a new file credentials store. +func NewFakeStore() credentials.Store { + return &fakeStore{store: map[string]types.AuthConfig{}} +} + +func (c *fakeStore) SetStore(store map[string]types.AuthConfig) { + c.store = store +} + +func (c *fakeStore) SetEraseFunc(eraseFunc func(string) error) { + c.eraseFunc = eraseFunc +} + +func (c *fakeStore) SetGetFunc(getFunc func(string) (types.AuthConfig, error)) { + c.getFunc = getFunc +} + +func (c *fakeStore) SetGetAllFunc(getAllFunc func() (map[string]types.AuthConfig, error)) { + c.getAllFunc = getAllFunc +} + +func (c *fakeStore) SetStoreFunc(storeFunc func(types.AuthConfig) error) { + c.storeFunc = storeFunc +} + +// Erase removes the given credentials from the map store +func (c *fakeStore) Erase(serverAddress string) error { + if c.eraseFunc != nil { + return c.eraseFunc(serverAddress) + } + delete(c.store, serverAddress) + return nil +} + +// Get retrieves credentials for a specific server from the map store. +func (c *fakeStore) Get(serverAddress string) (types.AuthConfig, error) { + if c.getFunc != nil { + return c.getFunc(serverAddress) + } + authConfig, _ := c.store[serverAddress] + return authConfig, nil +} + +func (c *fakeStore) GetAll() (map[string]types.AuthConfig, error) { + if c.getAllFunc != nil { + return c.getAllFunc() + } + return c.store, nil +} + +// Store saves the given credentials in the map store. +func (c *fakeStore) Store(authConfig types.AuthConfig) error { + if c.storeFunc != nil { + return c.storeFunc(authConfig) + } + c.store[authConfig.ServerAddress] = authConfig + return nil +}