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/container/opts.go b/cli/command/container/opts.go index 30c356a050..5c8e2c53c4 100644 --- a/cli/command/container/opts.go +++ b/cli/command/container/opts.go @@ -118,7 +118,6 @@ type containerOptions struct { runtime string autoRemove bool init bool - initPath string Image string Args []string @@ -230,10 +229,10 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { // Health-checking flags.StringVar(&copts.healthCmd, "health-cmd", "", "Command to run to check health") - flags.DurationVar(&copts.healthInterval, "health-interval", 0, "Time between running the check (ns|us|ms|s|m|h) (default 0s)") + flags.DurationVar(&copts.healthInterval, "health-interval", 0, "Time between running the check (ms|s|m|h) (default 0s)") flags.IntVar(&copts.healthRetries, "health-retries", 0, "Consecutive failures needed to report unhealthy") - flags.DurationVar(&copts.healthTimeout, "health-timeout", 0, "Maximum time to allow one check to run (ns|us|ms|s|m|h) (default 0s)") - flags.DurationVar(&copts.healthStartPeriod, "health-start-period", 0, "Start period for the container to initialize before starting health-retries countdown (ns|us|ms|s|m|h) (default 0s)") + flags.DurationVar(&copts.healthTimeout, "health-timeout", 0, "Maximum time to allow one check to run (ms|s|m|h) (default 0s)") + flags.DurationVar(&copts.healthStartPeriod, "health-start-period", 0, "Start period for the container to initialize before starting health-retries countdown (ms|s|m|h) (default 0s)") flags.SetAnnotation("health-start-period", "version", []string{"1.29"}) flags.BoolVar(&copts.noHealthcheck, "no-healthcheck", false, "Disable any container-specified HEALTHCHECK") @@ -284,8 +283,6 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.BoolVar(&copts.init, "init", false, "Run an init inside the container that forwards signals and reaps processes") flags.SetAnnotation("init", "version", []string{"1.25"}) - flags.StringVar(&copts.initPath, "init-path", "", "Path to the docker-init binary") - flags.SetAnnotation("init-path", "version", []string{"1.25"}) return copts } diff --git a/cli/command/formatter/stack.go b/cli/command/formatter/stack.go new file mode 100644 index 0000000000..676bcc05fe --- /dev/null +++ b/cli/command/formatter/stack.go @@ -0,0 +1,67 @@ +package formatter + +import ( + "strconv" +) + +const ( + defaultStackTableFormat = "table {{.Name}}\t{{.Services}}" + + stackServicesHeader = "SERVICES" +) + +// Stack contains deployed stack information. +type Stack struct { + // Name is the name of the stack + Name string + // Services is the number of the services + Services int +} + +// NewStackFormat returns a format for use with a stack Context +func NewStackFormat(source string) Format { + switch source { + case TableFormatKey: + return defaultStackTableFormat + } + return Format(source) +} + +// StackWrite writes formatted stacks using the Context +func StackWrite(ctx Context, stacks []*Stack) error { + render := func(format func(subContext subContext) error) error { + for _, stack := range stacks { + if err := format(&stackContext{s: stack}); err != nil { + return err + } + } + return nil + } + return ctx.Write(newStackContext(), render) +} + +type stackContext struct { + HeaderContext + s *Stack +} + +func newStackContext() *stackContext { + stackCtx := stackContext{} + stackCtx.header = map[string]string{ + "Name": nameHeader, + "Services": stackServicesHeader, + } + return &stackCtx +} + +func (s *stackContext) MarshalJSON() ([]byte, error) { + return marshalJSON(s) +} + +func (s *stackContext) Name() string { + return s.s.Name +} + +func (s *stackContext) Services() string { + return strconv.Itoa(s.s.Services) +} diff --git a/cli/command/formatter/stack_test.go b/cli/command/formatter/stack_test.go new file mode 100644 index 0000000000..b18ae7f083 --- /dev/null +++ b/cli/command/formatter/stack_test.go @@ -0,0 +1,64 @@ +package formatter + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStackContextWrite(t *testing.T) { + cases := []struct { + context Context + expected string + }{ + // Errors + { + Context{Format: "{{InvalidFunction}}"}, + `Template parsing error: template: :1: function "InvalidFunction" not defined +`, + }, + { + Context{Format: "{{nil}}"}, + `Template parsing error: template: :1:2: executing "" at : nil is not a command +`, + }, + // Table format + { + Context{Format: NewStackFormat("table")}, + `NAME SERVICES +baz 2 +bar 1 +`, + }, + { + Context{Format: NewStackFormat("table {{.Name}}")}, + `NAME +baz +bar +`, + }, + // Custom Format + { + Context{Format: NewStackFormat("{{.Name}}")}, + `baz +bar +`, + }, + } + + stacks := []*Stack{ + {Name: "baz", Services: 2}, + {Name: "bar", Services: 1}, + } + for _, testcase := range cases { + out := bytes.NewBufferString("") + testcase.context.Output = out + err := StackWrite(testcase.context, stacks) + if err != nil { + assert.Error(t, err, testcase.expected) + } else { + assert.Equal(t, out.String(), testcase.expected) + } + } +} diff --git a/cli/command/image/build.go b/cli/command/image/build.go index 5d4c2557c7..5ab3829bea 100644 --- a/cli/command/image/build.go +++ b/cli/command/image/build.go @@ -254,7 +254,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { } // Setup an upload progress bar - progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true) + progressOutput := streamformatter.NewProgressOutput(progBuff) if !dockerCli.Out().IsTerminal() { progressOutput = &lastProgressOutput{output: progressOutput} } diff --git a/cli/command/image/build/context.go b/cli/command/image/build/context.go index da8cf11e9e..e6165aa975 100644 --- a/cli/command/image/build/context.go +++ b/cli/command/image/build/context.go @@ -165,7 +165,7 @@ func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.Read if err != nil { return nil, "", errors.Errorf("unable to download remote context %s: %v", remoteURL, err) } - progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(out, true) + progressOutput := streamformatter.NewProgressOutput(out) // Pass the response body through a progress reader. progReader := progress.NewProgressReader(response.Body, progressOutput, response.ContentLength, "", fmt.Sprintf("Downloading build context from remote url: %s", remoteURL)) 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..8582c09b62 --- /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.True(t, match) + } + } +} 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..134722f0f1 --- /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, "image:local", ref) + 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, "test message", options.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, "ENV DEBUG true", options.Changes[0]) + 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..b3a2c2f5f5 --- /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, "image1", image) + } else { + assert.Equal(t, "image2", image) + } + 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, imageInspectInvocationCount, tc.imageCount) + } +} 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..070b19efb1 --- /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) + testutil.ErrorContains(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, "image", options.Filters.Get("reference")[0]) + return []types.ImageSummary{{}}, nil + }, + }, + { + name: "filters", + args: []string{"--filter", "name=value"}, + imageListFunc: func(options types.ImageListOptions) ([]types.ImageSummary, error) { + assert.Equal(t, "value", options.Filters.Get("name")[0]) + 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.True(t, cmd.HasAlias("images")) + assert.True(t, cmd.HasAlias("list")) + assert.False(t, cmd.HasAlias("other")) +} 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..ebe1a0c7e3 --- /dev/null +++ b/cli/command/image/load_test.go @@ -0,0 +1,105 @@ +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) + testutil.ErrorContains(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() + testutil.ErrorContains(t, err, 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..c17a75d224 --- /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) + testutil.ErrorContains(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, "false", pruneFilter.Get("dangling")[0]) + return types.ImagesPruneReport{}, nil + }, + }, + { + name: "force-deleted", + args: []string{"--force"}, + imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) { + assert.Equal(t, "true", pruneFilter.Get("dangling")[0]) + 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, "true", pruneFilter.Get("dangling")[0]) + 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..b4b57e2abc --- /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) + testutil.ErrorContains(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..b382ad7ee1 --- /dev/null +++ b/cli/command/image/push_test.go @@ -0,0 +1,85 @@ +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/pkg/testutil" + "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) + testutil.ErrorContains(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..0915729b20 --- /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.True(t, cmd.HasAlias("rmi")) + assert.True(t, cmd.HasAlias("remove")) + assert.False(t, cmd.HasAlias("other")) +} + +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.False(t, options.Force) + assert.True(t, options.PruneChildren) + 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) + testutil.ErrorContains(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, "image1", image) + 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, "image1", image) + 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..df34b3591d --- /dev/null +++ b/cli/command/image/save_test.go @@ -0,0 +1,100 @@ +package image + +import ( + "bytes" + "io" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/docker/cli/cli/internal/test" + "github.com/docker/docker/pkg/testutil" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +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) + testutil.ErrorContains(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) { + require.Len(t, images, 1) + assert.Equal(t, "arg1", images[0]) + 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) { + require.Len(t, images, 2) + assert.Equal(t, "arg1", images[0]) + assert.Equal(t, "arg2", images[1]) + 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..8cf0c534b8 --- /dev/null +++ b/cli/command/image/tag_test.go @@ -0,0 +1,44 @@ +package image + +import ( + "bytes" + "io/ioutil" + "testing" + + "github.com/docker/cli/cli/internal/test" + "github.com/docker/docker/pkg/testutil" + "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) + testutil.ErrorContains(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, "image1", image) + assert.Equal(t, "image2", ref) + return nil + }, + }, buf)) + cmd.SetArgs([]string{"image1", "image2"}) + cmd.SetOutput(ioutil.Discard) + assert.NoError(t, cmd.Execute()) + value, _ := cmd.Flags().GetBool("interspersed") + assert.False(t, value) +} 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..54855c6dc2 100644 --- a/cli/command/in.go +++ b/cli/command/in.go @@ -1,20 +1,18 @@ package command import ( + "errors" "io" "os" "runtime" "github.com/docker/docker/pkg/term" - "github.com/pkg/errors" ) // 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 +24,15 @@ 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 { + if os.Getenv("NORAW") != "" || !i.CommonStream.isTerminal { return nil } - i.state, err = term.SetRawTerminal(i.fd) + i.CommonStream.state, err = term.SetRawTerminal(i.CommonStream.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 +52,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..27b44c235d 100644 --- a/cli/command/out.go +++ b/cli/command/out.go @@ -11,42 +11,23 @@ import ( // 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 +// SetRawTerminal sets raw mode on the input terminal func (o *OutStream) SetRawTerminal() (err error) { - if os.Getenv("NORAW") != "" || !o.isTerminal { + if os.Getenv("NORAW") != "" || !o.CommonStream.isTerminal { return nil } - o.state, err = term.SetRawTerminalOutput(o.fd) + o.CommonStream.state, err = term.SetRawTerminalOutput(o.CommonStream.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 +46,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/service/list.go b/cli/command/service/list.go index d02c691a9b..338ac9164b 100644 --- a/cli/command/service/list.go +++ b/cli/command/service/list.go @@ -45,7 +45,8 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { ctx := context.Background() client := dockerCli.Client() - services, err := client.ServiceList(ctx, types.ServiceListOptions{}) + serviceFilters := opts.filter.Value() + services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceFilters}) if err != nil { return err } diff --git a/cli/command/service/logs.go b/cli/command/service/logs.go index c4ac557021..5db7c42bc3 100644 --- a/cli/command/service/logs.go +++ b/cli/command/service/logs.go @@ -92,7 +92,15 @@ func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { if !client.IsErrServiceNotFound(err) { return err } - task, _, _ := cli.TaskInspectWithRaw(ctx, opts.target) + task, _, err := cli.TaskInspectWithRaw(ctx, opts.target) + if err != nil { + if client.IsErrTaskNotFound(err) { + // if the task isn't found, rewrite the error to be clear + // that we looked for services AND tasks and found none + err = fmt.Errorf("no such task or service") + } + return err + } tty = task.Spec.ContainerSpec.TTY // TODO(dperny) hot fix until we get a nice details system squared away, // ignores details (including task context) if we have a TTY log @@ -104,15 +112,10 @@ func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { responseBody, err = cli.TaskLogs(ctx, opts.target, options) if err != nil { - if client.IsErrTaskNotFound(err) { - // if the task ALSO isn't found, rewrite the error to be clear - // that we looked for services AND tasks - err = fmt.Errorf("No such task or service") - } return err } + maxLength = getMaxLength(task.Slot) - responseBody, _ = cli.TaskLogs(ctx, opts.target, options) } else { tty = service.Spec.TaskTemplate.ContainerSpec.TTY // TODO(dperny) hot fix until we get a nice details system squared away, diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index ed23a562fe..d043b78cf8 100644 --- a/cli/command/service/opts.go +++ b/cli/command/service/opts.go @@ -802,13 +802,13 @@ func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions, defaultFlagValu flags.StringVar(&opts.healthcheck.cmd, flagHealthCmd, "", "Command to run to check health") flags.SetAnnotation(flagHealthCmd, "version", []string{"1.25"}) - flags.Var(&opts.healthcheck.interval, flagHealthInterval, "Time between running the check (ns|us|ms|s|m|h)") + flags.Var(&opts.healthcheck.interval, flagHealthInterval, "Time between running the check (ms|s|m|h)") flags.SetAnnotation(flagHealthInterval, "version", []string{"1.25"}) - flags.Var(&opts.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run (ns|us|ms|s|m|h)") + flags.Var(&opts.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run (ms|s|m|h)") flags.SetAnnotation(flagHealthTimeout, "version", []string{"1.25"}) flags.IntVar(&opts.healthcheck.retries, flagHealthRetries, 0, "Consecutive failures needed to report unhealthy") flags.SetAnnotation(flagHealthRetries, "version", []string{"1.25"}) - flags.Var(&opts.healthcheck.startPeriod, flagHealthStartPeriod, "Start period for the container to initialize before counting retries towards unstable (ns|us|ms|s|m|h)") + flags.Var(&opts.healthcheck.startPeriod, flagHealthStartPeriod, "Start period for the container to initialize before counting retries towards unstable (ms|s|m|h)") flags.SetAnnotation(flagHealthStartPeriod, "version", []string{"1.29"}) flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") flags.SetAnnotation(flagNoHealthcheck, "version", []string{"1.25"}) diff --git a/cli/command/service/progress/progress.go b/cli/command/service/progress/progress.go index 61c040df5e..266a949814 100644 --- a/cli/command/service/progress/progress.go +++ b/cli/command/service/progress/progress.go @@ -63,7 +63,7 @@ func stateToProgress(state swarm.TaskState, rollback bool) int64 { func ServiceProgress(ctx context.Context, client client.APIClient, serviceID string, progressWriter io.WriteCloser) error { defer progressWriter.Close() - progressOut := streamformatter.NewJSONStreamFormatter().NewProgressOutput(progressWriter, false) + progressOut := streamformatter.NewJSONProgressOutput(progressWriter, false) sigint := make(chan os.Signal, 1) signal.Notify(sigint, os.Interrupt) diff --git a/cli/command/stack/common.go b/cli/command/stack/common.go index 443ca3297d..3e6b57bd8b 100644 --- a/cli/command/stack/common.go +++ b/cli/command/stack/common.go @@ -18,9 +18,7 @@ func getStackFilter(namespace string) filters.Args { } func getServiceFilter(namespace string) filters.Args { - filter := getStackFilter(namespace) - filter.Add("runtime", string(swarm.RuntimeContainer)) - return filter + return getStackFilter(namespace) } func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) filters.Args { diff --git a/cli/command/stack/deploy_composefile.go b/cli/command/stack/deploy_composefile.go index f241310bf2..e3f59de14c 100644 --- a/cli/command/stack/deploy_composefile.go +++ b/cli/command/stack/deploy_composefile.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "os" + "path/filepath" "sort" "strings" @@ -20,7 +21,7 @@ import ( ) func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error { - configDetails, err := getConfigDetails(opts) + configDetails, err := getConfigDetails(opts.composefile) if err != nil { return err } @@ -108,16 +109,16 @@ func propertyWarnings(properties map[string]string) string { return strings.Join(msgs, "\n\n") } -func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) { +func getConfigDetails(composefile string) (composetypes.ConfigDetails, error) { var details composetypes.ConfigDetails - var err error - details.WorkingDir, err = os.Getwd() + absPath, err := filepath.Abs(composefile) if err != nil { return details, err } + details.WorkingDir = filepath.Dir(absPath) - configFile, err := getConfigFile(opts.composefile) + configFile, err := getConfigFile(composefile) if err != nil { return details, err } diff --git a/cli/command/stack/deploy_composefile_test.go b/cli/command/stack/deploy_composefile_test.go new file mode 100644 index 0000000000..d5ef5463ff --- /dev/null +++ b/cli/command/stack/deploy_composefile_test.go @@ -0,0 +1,28 @@ +package stack + +import ( + "os" + "path/filepath" + "testing" + + "github.com/docker/docker/pkg/testutil/tempfile" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetConfigDetails(t *testing.T) { + content := ` +version: "3.0" +services: + foo: + image: alpine:3.5 +` + file := tempfile.NewTempFile(t, "test-get-config-details", content) + defer file.Remove() + + details, err := getConfigDetails(file.Name()) + require.NoError(t, err) + assert.Equal(t, filepath.Dir(file.Name()), details.WorkingDir) + assert.Len(t, details.ConfigFiles, 1) + assert.Len(t, details.Environment, len(os.Environ())) +} diff --git a/cli/command/stack/list.go b/cli/command/stack/list.go index 7b1f8e3559..c1402b3005 100644 --- a/cli/command/stack/list.go +++ b/cli/command/stack/list.go @@ -1,14 +1,11 @@ package stack import ( - "fmt" - "io" "sort" - "strconv" - "text/tabwriter" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/compose/convert" "github.com/docker/cli/client" "github.com/docker/docker/api/types" @@ -17,11 +14,8 @@ import ( "golang.org/x/net/context" ) -const ( - listItemFmt = "%s\t%s\n" -) - type listOptions struct { + format string } func newListCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -37,6 +31,8 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { }, } + flags := cmd.Flags() + flags.StringVar(&opts.format, "format", "", "Pretty-print stacks using a Go template") return cmd } @@ -48,55 +44,32 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { if err != nil { return err } - - out := dockerCli.Out() - printTable(out, stacks) - return nil + format := opts.format + if len(format) == 0 { + format = formatter.TableFormatKey + } + stackCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewStackFormat(format), + } + sort.Sort(byName(stacks)) + return formatter.StackWrite(stackCtx, stacks) } -type byName []*stack +type byName []*formatter.Stack func (n byName) Len() int { return len(n) } func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] } func (n byName) Less(i, j int) bool { return n[i].Name < n[j].Name } -func printTable(out io.Writer, stacks []*stack) { - writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0) - - // Ignore flushing errors - defer writer.Flush() - - sort.Sort(byName(stacks)) - - fmt.Fprintf(writer, listItemFmt, "NAME", "SERVICES") - for _, stack := range stacks { - fmt.Fprintf( - writer, - listItemFmt, - stack.Name, - strconv.Itoa(stack.Services), - ) - } -} - -type stack struct { - // Name is the name of the stack - Name string - // Services is the number of the services - Services int -} - -func getStacks( - ctx context.Context, - apiclient client.APIClient, -) ([]*stack, error) { +func getStacks(ctx context.Context, apiclient client.APIClient) ([]*formatter.Stack, error) { services, err := apiclient.ServiceList( ctx, types.ServiceListOptions{Filters: getAllStacksFilter()}) if err != nil { return nil, err } - m := make(map[string]*stack, 0) + m := make(map[string]*formatter.Stack, 0) for _, service := range services { labels := service.Spec.Labels name, ok := labels[convert.LabelNamespace] @@ -106,7 +79,7 @@ func getStacks( } ztack, ok := m[name] if !ok { - m[name] = &stack{ + m[name] = &formatter.Stack{ Name: name, Services: 1, } @@ -114,7 +87,7 @@ func getStacks( ztack.Services++ } } - var stacks []*stack + var stacks []*formatter.Stack for _, stack := range m { stacks = append(stacks, stack) } diff --git a/cli/command/stream.go b/cli/command/stream.go new file mode 100644 index 0000000000..71a43fa2e9 --- /dev/null +++ b/cli/command/stream.go @@ -0,0 +1,34 @@ +package command + +import ( + "github.com/docker/docker/pkg/term" +) + +// 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 +} + +// 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/init.go b/cli/command/swarm/init.go index e64a2c5ae6..ea3189a0c7 100644 --- a/cli/command/swarm/init.go +++ b/cli/command/swarm/init.go @@ -19,6 +19,7 @@ type initOptions struct { listenAddr NodeAddrOption // Not a NodeAddrOption because it has no default port. advertiseAddr string + dataPathAddr string forceNewCluster bool availability string } @@ -40,6 +41,7 @@ func newInitCommand(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: [:port])") flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") + flags.StringVar(&opts.dataPathAddr, flagDataPathAddr, "", "Address or interface to use for data path traffic (format: )") flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state") flags.BoolVar(&opts.autolock, flagAutolock, false, "Enable manager autolocking (requiring an unlock key to start a stopped manager)") flags.StringVar(&opts.availability, flagAvailability, "active", `Availability of the node ("active"|"pause"|"drain")`) @@ -54,6 +56,7 @@ func runInit(dockerCli command.Cli, flags *pflag.FlagSet, opts initOptions) erro req := swarm.InitRequest{ ListenAddr: opts.listenAddr.String(), AdvertiseAddr: opts.advertiseAddr, + DataPathAddr: opts.dataPathAddr, ForceNewCluster: opts.forceNewCluster, Spec: opts.swarmOptions.ToSpec(flags), AutoLockManagers: opts.swarmOptions.autolock, diff --git a/cli/command/swarm/join.go b/cli/command/swarm/join.go index ff4351c52e..0f09527d07 100644 --- a/cli/command/swarm/join.go +++ b/cli/command/swarm/join.go @@ -19,6 +19,7 @@ type joinOptions struct { listenAddr NodeAddrOption // Not a NodeAddrOption because it has no default port. advertiseAddr string + dataPathAddr string token string availability string } @@ -41,6 +42,7 @@ func newJoinCommand(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: [:port])") flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") + flags.StringVar(&opts.dataPathAddr, flagDataPathAddr, "", "Address or interface to use for data path traffic (format: )") flags.StringVar(&opts.token, flagToken, "", "Token for entry into the swarm") flags.StringVar(&opts.availability, flagAvailability, "active", `Availability of the node ("active"|"pause"|"drain")`) return cmd @@ -54,6 +56,7 @@ func runJoin(dockerCli command.Cli, flags *pflag.FlagSet, opts joinOptions) erro JoinToken: opts.token, ListenAddr: opts.listenAddr.String(), AdvertiseAddr: opts.advertiseAddr, + DataPathAddr: opts.dataPathAddr, RemoteAddrs: []string{opts.remote}, } if flags.Changed(flagAvailability) { diff --git a/cli/command/swarm/join_token.go b/cli/command/swarm/join_token.go index 0a905dfc26..b35efad8a9 100644 --- a/cli/command/swarm/join_token.go +++ b/cli/command/swarm/join_token.go @@ -108,10 +108,10 @@ func printJoinCommand(ctx context.Context, dockerCli command.Cli, nodeID string, if node.ManagerStatus != nil { if worker { - fmt.Fprintf(dockerCli.Out(), "To add a worker to this swarm, run the following command:\n\n docker swarm join \\\n --token %s \\\n %s\n\n", sw.JoinTokens.Worker, node.ManagerStatus.Addr) + fmt.Fprintf(dockerCli.Out(), "To add a worker to this swarm, run the following command:\n\n docker swarm join --token %s %s\n\n", sw.JoinTokens.Worker, node.ManagerStatus.Addr) } if manager { - fmt.Fprintf(dockerCli.Out(), "To add a manager to this swarm, run the following command:\n\n docker swarm join \\\n --token %s \\\n %s\n\n", sw.JoinTokens.Manager, node.ManagerStatus.Addr) + fmt.Fprintf(dockerCli.Out(), "To add a manager to this swarm, run the following command:\n\n docker swarm join --token %s %s\n\n", sw.JoinTokens.Manager, node.ManagerStatus.Addr) } } diff --git a/cli/command/swarm/opts.go b/cli/command/swarm/opts.go index 225d38d110..d2097050af 100644 --- a/cli/command/swarm/opts.go +++ b/cli/command/swarm/opts.go @@ -2,7 +2,9 @@ package swarm import ( "encoding/csv" + "encoding/pem" "fmt" + "io/ioutil" "strings" "time" @@ -19,6 +21,7 @@ const ( flagDispatcherHeartbeat = "dispatcher-heartbeat" flagListenAddr = "listen-addr" flagAdvertiseAddr = "advertise-addr" + flagDataPathAddr = "data-path-addr" flagQuiet = "quiet" flagRotate = "rotate" flagToken = "token" @@ -154,6 +157,15 @@ func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) { case "url": hasURL = true externalCA.URL = value + case "cacert": + cacontents, err := ioutil.ReadFile(value) + if err != nil { + return nil, errors.Wrap(err, "unable to read CA cert for external CA") + } + if pemBlock, _ := pem.Decode(cacontents); pemBlock == nil { + return nil, errors.New("CA cert for external CA must be in PEM format") + } + externalCA.CACert = string(cacontents) default: externalCA.Options[key] = value } diff --git a/cli/command/swarm/testdata/jointoken-manager-rotate.golden b/cli/command/swarm/testdata/jointoken-manager-rotate.golden index 7ee455bec8..b4d0a48f66 100644 --- a/cli/command/swarm/testdata/jointoken-manager-rotate.golden +++ b/cli/command/swarm/testdata/jointoken-manager-rotate.golden @@ -2,7 +2,4 @@ Successfully rotated manager join token. To add a manager to this swarm, run the following command: - docker swarm join \ - --token manager-join-token \ - 127.0.0.1 - + docker swarm join --token manager-join-token 127.0.0.1 diff --git a/cli/command/swarm/testdata/jointoken-manager.golden b/cli/command/swarm/testdata/jointoken-manager.golden index d56527aa55..522b2968fe 100644 --- a/cli/command/swarm/testdata/jointoken-manager.golden +++ b/cli/command/swarm/testdata/jointoken-manager.golden @@ -1,6 +1,3 @@ To add a manager to this swarm, run the following command: - docker swarm join \ - --token manager-join-token \ - 127.0.0.1 - + docker swarm join --token manager-join-token 127.0.0.1 diff --git a/cli/command/swarm/testdata/jointoken-worker.golden b/cli/command/swarm/testdata/jointoken-worker.golden index 5d44f3daee..899df703fd 100644 --- a/cli/command/swarm/testdata/jointoken-worker.golden +++ b/cli/command/swarm/testdata/jointoken-worker.golden @@ -1,6 +1,3 @@ To add a worker to this swarm, run the following command: - docker swarm join \ - --token worker-join-token \ - 127.0.0.1 - + docker swarm join --token worker-join-token 127.0.0.1 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/system/info.go b/cli/command/system/info.go index 8c5dec3f5a..c951e42eb7 100644 --- a/cli/command/system/info.go +++ b/cli/command/system/info.go @@ -91,6 +91,10 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { fmt.Fprintf(dockerCli.Out(), "\n") } + fmt.Fprintf(dockerCli.Out(), " Log:") + fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Log, " ")) + fmt.Fprintf(dockerCli.Out(), "\n") + fmt.Fprintf(dockerCli.Out(), "Swarm: %v\n", info.Swarm.LocalNodeState) if info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive && info.Swarm.LocalNodeState != swarm.LocalNodeStateLocked { fmt.Fprintf(dockerCli.Out(), " NodeID: %s\n", info.Swarm.NodeID) 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/compose/schema/bindata.go b/cli/compose/schema/bindata.go index fa9a29f96c..7ce0fc0113 100644 --- a/cli/compose/schema/bindata.go +++ b/cli/compose/schema/bindata.go @@ -110,7 +110,7 @@ func dataConfig_schema_v31Json() (*asset, error) { return a, nil } -var _dataConfig_schema_v32Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x1b\x4d\x73\xdc\xa8\xf2\x3e\xbf\x42\xa5\xe4\x16\x7f\xa4\xde\x4b\xbd\xaa\x97\xdb\x3b\xbe\xd3\xee\x79\x5d\x13\x15\x83\x7a\x34\xc4\x12\x10\x40\x63\x4f\x52\xfe\xef\x5b\xfa\x1c\x40\x20\xd0\x8c\x1c\x67\xb7\xf6\x64\x5b\x74\x37\xf4\x77\x37\x8d\x7f\x6c\x92\x24\x7d\x2f\xf1\x01\x2a\x94\x7e\x4e\xd2\x83\x52\xfc\xf3\xfd\xfd\x57\xc9\xe8\x6d\xf7\xf5\x8e\x89\xe2\x3e\x17\x68\xaf\x6e\x3f\x7e\xba\xef\xbe\xbd\x4b\x6f\x1a\x3c\x92\x37\x28\x98\xd1\x3d\x29\xb2\x6e\x25\x3b\xfe\xfb\xee\x5f\x77\x0d\x7a\x07\xa2\x4e\x1c\x1a\x20\xb6\xfb\x0a\x58\x75\xdf\x04\x7c\xab\x89\x80\x06\xf9\x21\x3d\x82\x90\x84\xd1\x74\x7b\xb3\x69\xd6\xb8\x60\x1c\x84\x22\x20\xd3\xcf\x49\x73\xb8\x24\x19\x41\x86\x0f\x1a\x59\xa9\x04\xa1\x45\xda\x7e\x7e\x69\x29\x24\x49\x2a\x41\x1c\x09\xd6\x28\x8c\x47\x7d\x77\x7f\xa6\x7f\x3f\x82\xdd\xd8\x54\xb5\xc3\xb6\xdf\x39\x52\x0a\x04\xfd\x7d\x7a\xb6\x76\xf9\xcb\x03\xba\xfd\xfe\xbf\xdb\x3f\x3e\xde\xfe\xf7\x2e\xbb\xdd\x7e\x78\x6f\x2c\x37\xf2\x15\xb0\xef\xb6\xcf\x61\x4f\x28\x51\x84\xd1\x71\xff\x74\x84\x7c\xe9\x7f\x7b\x19\x37\x46\x79\xde\x02\xa3\xd2\xd8\x7b\x8f\x4a\x09\x26\xcf\x14\xd4\x13\x13\x8f\x21\x9e\x47\xb0\x37\xe2\xb9\xdf\xdf\xc1\xb3\xc9\xce\x91\x95\x75\x15\xd4\xe0\x00\xf5\x46\xcc\x74\xdb\xaf\xa3\x3f\x09\x58\x80\x0a\x9b\x6c\x07\xf5\x66\x16\xdb\x6c\x7f\x1d\xc3\x9b\x81\xe9\x59\xd8\x0e\x42\xdb\xbb\x3d\xa0\xe1\xde\x2e\x51\xb9\xdc\xcb\x2f\xab\x51\x58\x1e\x29\xe5\xc0\x4b\x76\x6a\xbe\x79\xe4\xd1\x01\x54\x40\x55\x3a\x8a\x20\x49\xd2\x5d\x4d\xca\xdc\x96\x28\xa3\xf0\x5b\x43\xe2\x41\xfb\x98\x24\x3f\xec\x48\xa6\xd1\x69\xd7\x8d\xbf\xfc\x0a\x1f\xd7\x3d\xbc\x8c\xeb\x98\x51\x05\xcf\xaa\x65\x6a\x7e\xeb\x4e\x04\x0c\x3f\x82\xd8\x93\x12\x62\x31\x90\x28\xe4\x8c\xc8\x4a\x22\x55\xc6\x44\x96\x13\xac\x9c\xf8\x18\xe1\x03\x64\x7b\xc1\xaa\x20\x95\x7d\xd6\x9d\x43\xa6\x2f\x16\x9d\x09\xe1\xb0\x61\x8e\xa8\xda\x5f\xdb\x8d\x83\x60\x8a\x11\xcf\x50\x9e\x1b\x02\x41\x42\xa0\x53\x7a\x93\xa4\x44\x41\x25\xdd\xb2\x4a\xd2\x9a\x92\x6f\x35\xfc\xbf\x07\x51\xa2\x06\x9b\x6e\x2e\x18\x5f\x9f\x70\x21\x58\xcd\x33\x8e\x44\x63\xa9\xf3\x7a\x4c\x31\xab\x2a\x44\xd7\x32\xdf\x25\x7c\x44\x48\x9e\x51\x85\x08\x05\x91\x51\x54\x85\x2c\xb2\x71\x5f\xa0\xb9\xcc\xba\xca\x21\xd6\x92\x0c\x02\x63\x19\xb1\xaa\x3e\x72\x3a\xe7\x21\x1d\x99\xc6\x47\x9a\xb3\xa5\x16\x62\x26\x01\x09\x7c\xb8\x10\x9f\x55\x88\xd0\x18\xd9\x01\x55\xe2\xc4\x19\xe9\xec\xe5\x97\x33\x04\xa0\xc7\x6c\x0c\x4a\x8b\xc5\x00\xf4\x48\x04\xa3\xd5\xe0\x0d\x71\x91\x4a\xc3\x7f\xe6\x4c\x82\x2d\x18\x8b\x41\x7d\x69\x64\xd5\x90\xc9\x80\xf1\x30\x30\x7e\x93\xa4\xb4\xae\x76\x20\x9a\x62\xd8\x80\xdc\x33\x51\xa1\xe6\xb0\xc3\xde\xda\xb2\x21\x69\x87\xe5\xe9\x02\xd4\x79\x68\xea\x03\x54\x66\x25\xa1\x8f\xeb\x9b\x38\x3c\x2b\x81\xb2\x03\x93\xea\x92\x64\x90\x1e\x00\x95\xea\x80\x0f\x80\x1f\x67\xd0\x75\x28\x03\x9b\x49\x15\x63\xe4\xa4\x42\x45\x18\x88\xe3\x10\x48\x89\x76\x50\x5e\xc4\xe7\xaa\xc2\xd7\xc8\xb2\xa2\x68\x40\x7d\x16\x37\x29\x81\xfa\xe5\x50\xf1\x90\x0b\x72\x04\x11\x5b\x09\x30\x7e\xae\xdc\xec\xc5\x70\x25\x93\x84\xcb\x58\x03\xf4\xcb\x5d\x57\xc5\xce\x78\x55\xfb\x5b\x59\xa6\x5b\xbb\x5c\x48\xac\xbc\xef\xfa\x62\x71\x18\x57\x50\x18\x5a\xa9\x10\x6e\xea\x06\x01\xd2\xa3\xd7\x33\x68\xdf\x26\x65\x15\xcb\x7d\x06\x3a\x01\xb6\x65\xe3\x8d\xd4\x8b\x13\x61\x72\x51\x21\x1a\xa5\xba\x60\x27\x12\xe0\xc6\x77\xbc\xd8\x63\x9e\x8f\x1b\x36\xb1\x16\x0e\x95\x04\x49\x08\x3b\xbb\x57\x90\x06\x35\xc2\x8f\x9f\x22\x6d\xc2\x85\xfb\x9f\x59\x5c\x0f\xaa\x97\x66\x7c\x8d\x1c\x20\x75\x3e\x4a\xeb\x6e\xae\x83\x6c\x03\xde\xf6\xca\x25\x3c\x27\xb9\x3f\x56\xb4\x11\x42\x77\x30\xce\x84\x9a\x78\xd7\xf2\x74\xef\xb3\x60\x5d\x5c\x43\x9c\x3a\x27\xfc\x6e\xf3\x89\x34\x26\xea\x8e\x42\x9a\xfa\x5f\xd0\x3f\xc2\x9e\x91\xce\x44\x29\x07\xb4\x42\xa2\x00\xb3\x0d\x21\x54\x41\x01\xc2\x83\xc0\xeb\x5d\x49\xe4\x01\xf2\x25\x38\x82\x29\x86\x59\x19\xe7\x18\xce\x3e\x36\xde\x19\x4c\x82\xdb\xab\x6b\x33\x2e\xc8\x91\x94\x50\x58\x1c\xef\x18\x2b\x01\x51\x23\x51\x08\x40\x79\xc6\x68\x79\x8a\x80\x94\x0a\x89\x60\xfb\x27\x01\xd7\x82\xa8\x53\xc6\xb8\x5a\xbd\x2a\x94\x87\x2a\x93\xe4\x3b\x98\xbe\x77\xb6\xfa\x9e\xd0\xd6\x3a\x90\x75\x31\x96\xbc\x96\xfb\xf9\xcc\xf6\x95\xdc\x46\xb2\x5a\xe0\xeb\x1c\x67\x16\xbe\x36\x83\xdc\x3c\x70\xb1\x04\x78\xe2\xf0\xbd\x0a\x43\x35\xd4\xac\xab\x38\x03\xb5\x3c\x49\xac\x2e\xab\xad\xa5\xca\x09\xcd\x18\x07\x1a\xf4\x0d\xa9\x18\xcf\x0a\x81\x30\x64\x1c\x04\x61\x4e\x51\x18\x01\x36\xaf\x05\x6a\xf6\x9f\x92\x91\xa4\xa0\xc8\x1d\x77\x34\x50\x55\xf1\xfd\x85\x97\x00\x4a\x85\x9d\xbd\x2e\x49\x45\xfc\x4e\xe3\xb0\xda\x88\x7a\xad\xab\xd5\xdc\x25\xda\x4c\x79\x16\x15\xb2\x67\x3a\x84\xf9\x06\x21\xa2\x33\x38\x20\xb1\x20\x75\xb4\x8e\xb9\xf7\xe4\x27\x57\xdf\xe0\x3c\x97\x31\xe2\x6a\xe9\xdd\xf4\x07\xd9\x3a\xe1\x17\x95\x5e\xf6\x31\xb6\xde\xea\xc7\xed\x54\xb5\x0c\x36\x71\x2d\x0c\x95\x73\x0d\xc8\x08\x3a\x9d\xd5\x24\x7f\x89\x08\x6d\xe8\xa8\x05\x77\xe8\x26\x22\x8e\xf7\x3b\x45\xc6\xce\xd7\x8e\xfa\xd1\x15\x81\x86\x83\x19\x95\x44\x2a\xa0\xf8\x14\xbf\xd1\x8e\x4c\x6e\x89\xa7\x42\x99\xef\xbb\xe2\xba\xae\x16\x0a\x15\x5d\xbc\x8d\x6e\x74\xe2\x7d\xb5\x1f\xe3\xfd\x14\x56\x28\xc3\x8c\x7b\x54\x13\xcf\xc6\xd2\x34\x6b\x5d\x5d\xcc\xd4\xa1\xbe\x90\xf1\xc4\xc4\x63\x93\x90\x72\xe2\x8e\x1c\x1b\x0b\x65\xc1\xe4\xd3\xba\xeb\x1b\x08\xb8\x46\x7a\x3a\x68\x70\x04\x3a\x3f\x5e\xec\x81\xbc\xa3\x3f\x22\xd1\xce\x1a\x7a\xb9\x12\x6d\x93\x19\xc4\x31\x9c\xef\x05\x28\x41\xac\x51\xc2\x50\x34\xe9\xb9\x1d\xe4\xaf\x79\xe1\xae\x48\x05\xac\x76\x87\xa1\x8d\x6e\x38\x3d\x52\xaa\x8d\x46\x03\x4a\xd5\x20\x6d\x9d\x3e\x8c\x4a\x1d\xfa\xf2\xa0\xe2\x62\x12\x16\xd0\xbc\x1d\x6d\x44\x65\x37\x01\xbc\x24\x18\xc9\x50\x05\x71\xc5\x2d\x70\xcd\x73\xa4\x20\xeb\xde\xd1\x2c\xaa\xd9\x66\x8a\x35\x8e\x04\x2a\x4b\x28\x89\xac\x62\x8a\x9f\x34\x87\x12\x39\xa3\x7f\xb0\xee\x6d\xd1\xf7\x88\x94\xb5\x80\x0c\x61\x6f\x98\xb6\x30\x2a\x46\x89\x62\xce\x70\x12\xb7\x65\x85\x9e\xb3\x61\xdb\x16\x24\xd4\x92\x98\xdd\x78\xec\x05\xae\x66\x09\x5d\xee\x5e\x56\x56\xcf\xa8\xe8\x5c\xa4\x7b\x2c\x66\xd8\x71\xc2\xba\x00\xd9\x84\x9d\xf1\x7e\x3d\x88\x1f\x0c\xf0\xfd\xf5\x40\xc6\x59\x49\xba\x2a\x60\x0d\x0e\x31\xa3\x9d\x90\x63\x0c\xe2\x4a\x0b\x6c\xcc\xa1\xe9\x61\x2a\xae\x82\xce\xda\x22\x3c\x11\x9a\xb3\xa7\x05\x1b\xae\x67\x4a\xbc\x44\x18\xac\xe0\x78\xad\xa0\xa5\x12\x88\x50\xb5\x78\x9c\x74\x2d\x5b\x57\xa4\xfe\xd1\x3e\x03\x29\x62\x84\x0b\x26\x7d\x5f\x5a\xc0\xbc\x0e\x0e\x5d\x2a\xa8\x98\x70\x17\xc0\x57\xf0\x38\xbc\x78\x0b\xb0\x38\x80\xad\x90\x02\xa3\xa6\x74\x3d\x54\xc6\xf8\xfa\xd7\x04\xe1\x49\xdc\x36\x1c\x90\x08\x47\xd5\x5a\xde\x11\x3d\xb7\x4c\x9d\x39\x38\x99\x6f\x67\x13\x7f\x4b\x1b\x3a\x75\xf8\xec\x3d\x84\xac\x77\xd4\xd3\x05\x4e\x9b\x81\x35\x6f\xb3\x57\x0c\x7a\xc3\x93\x03\x8f\x56\x1f\xc6\x02\xfb\x66\x94\xd5\x36\x5a\xc5\xde\x79\xff\x7a\xe7\x6f\x6b\x7d\xfb\x6e\xcf\xd5\x14\x20\xa5\x10\x3e\x44\xf5\x0f\x0b\x8b\xc6\x2b\xe2\xd0\xa4\xcb\x75\x86\xa1\x1e\xea\x9f\x28\xf4\x37\xb1\xd9\x9f\x67\x5f\xfd\xdb\xe0\xe0\xa3\xdc\x16\xea\xe2\x3c\x1e\xf1\x12\xf5\x17\xd0\xd9\x5b\xab\xc2\x1c\x1e\x68\x2a\x99\xde\x25\xcc\x49\x72\xe9\xeb\xdb\xad\x79\x0c\x1b\xcc\xf1\xef\x1b\x66\x32\x9d\x1b\x2d\x0e\x20\x9e\xbb\x2b\x6b\xd3\x5e\x88\xf3\x9c\xaf\x18\x6c\xee\x3e\xcc\x94\x0c\x73\x2f\x91\x5e\x29\xd7\xae\x30\xb6\x75\xeb\xd4\xea\x33\x06\xe9\x4e\x9f\xe4\x7b\xfc\x5f\xc3\x9f\x3c\xd0\x6f\xf8\xa4\xa7\xc9\x5d\xd7\x0f\xf3\xa2\xbe\x7b\x5c\xbf\x35\xe4\x63\x81\x74\xef\xfa\xb4\xe8\xbe\xd5\x5b\x2f\x9f\x1a\x9d\xcf\xf6\xed\x31\xc1\xf0\x7c\xde\x33\xb9\xdc\xe8\x3f\xdb\x7f\x75\xd8\xbc\x6c\xfe\x0c\x00\x00\xff\xff\xa9\x16\x7b\x3d\x63\x35\x00\x00") +var _dataConfig_schema_v32Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x1b\x4d\x73\xdc\xa8\xf2\x3e\xbf\x42\xa5\xe4\x16\x7f\xa4\xde\x4b\xbd\xaa\x97\xdb\x3b\xbe\xd3\xee\x79\x5d\x13\x15\x83\x7a\x34\xc4\x12\x10\x40\x63\x4f\x52\xfe\xef\x5b\xfa\x1c\x40\x20\xd0\x8c\x1c\x67\xb7\xf6\x64\x5b\x74\x37\xf4\x77\x37\x8d\x7f\x6c\x92\x24\x7d\x2f\xf1\x01\x2a\x94\x7e\x4e\xd2\x83\x52\xfc\xf3\xfd\xfd\x57\xc9\xe8\x6d\xf7\xf5\x8e\x89\xe2\x3e\x17\x68\xaf\x6e\x3f\x7e\xba\xef\xbe\xbd\x4b\x6f\x1a\x3c\x92\x37\x28\x98\xd1\x3d\x29\xb2\x6e\x25\x3b\xfe\xfb\xee\x5f\x77\x0d\x7a\x07\xa2\x4e\x1c\x1a\x20\xb6\xfb\x0a\x58\x75\xdf\x04\x7c\xab\x89\x80\x06\xf9\x21\x3d\x82\x90\x84\xd1\x74\x7b\xb3\x69\xd6\xb8\x60\x1c\x84\x22\x20\xd3\xcf\x49\x73\xb8\x24\x19\x41\x86\x0f\x1a\x59\xa9\x04\xa1\x45\xda\x7e\x7e\x69\x29\x24\x49\x2a\x41\x1c\x09\xd6\x28\x8c\x47\x7d\x77\x7f\xa6\x7f\x3f\x82\xdd\xd8\x54\xb5\xc3\xb6\xdf\x39\x52\x0a\x04\xfd\x7d\x7a\xb6\x76\xf9\xcb\x03\xba\xfd\xfe\xbf\xdb\x3f\x3e\xde\xfe\xf7\x2e\xbb\xdd\x7e\x78\x6f\x2c\x37\xf2\x15\xb0\xef\xb6\xcf\x61\x4f\x28\x51\x84\xd1\x71\xff\x74\x84\x7c\xe9\x7f\x7b\x19\x37\x46\x79\xde\x02\xa3\xd2\xd8\x7b\x8f\x4a\x09\x26\xcf\x14\xd4\x13\x13\x8f\x21\x9e\x47\xb0\x37\xe2\xb9\xdf\xdf\xc1\xb3\xc9\xce\x91\x95\x75\x15\xd4\xe0\x00\xf5\x46\xcc\x74\xdb\xaf\xa3\x3f\x09\x58\x80\x0a\x9b\x6c\x07\xf5\x66\x16\xdb\x6c\x7f\x1d\xc3\x9b\x81\xe9\x59\xd8\x0e\x42\xdb\xbb\x3d\xa0\xe1\xde\x2e\x51\xb9\xdc\xcb\x2f\xab\x51\x58\x1e\x29\xe5\xc0\x4b\x76\x6a\xbe\x79\xe4\xd1\x01\x54\x40\x55\x3a\x8a\x20\x49\xd2\x5d\x4d\xca\xdc\x96\x28\xa3\xf0\x5b\x43\xe2\x41\xfb\x98\x24\x3f\xec\x48\xa6\xd1\x69\xd7\x8d\xbf\xfc\x0a\x1f\xd7\x3d\xbc\x8c\xeb\x98\x51\x05\xcf\xaa\x65\x6a\x7e\xeb\x4e\x04\x0c\x3f\x82\xd8\x93\x12\x62\x31\x90\x28\xe4\x8c\xc8\x4a\x22\x55\xc6\x44\x96\x13\xac\x9c\xf8\x25\xda\x41\x79\x15\x05\x8c\xf0\x01\xb2\xbd\x60\x55\x90\xca\x3e\xeb\x38\x91\xe9\x8b\x45\x67\x42\x38\x6c\xda\x23\xaa\xf6\xd7\x76\xe3\x20\x98\x62\xc4\x33\x94\xe7\x86\x48\x91\x10\xe8\x94\xde\x24\x29\x51\x50\x49\xb7\xb4\x93\xb4\xa6\xe4\x5b\x0d\xff\xef\x41\x94\xa8\xc1\xa6\x9b\x0b\xc6\xd7\x27\x5c\x08\x56\xf3\x8c\x23\xd1\xd8\xfa\xbc\x25\xa4\x98\x55\x15\xa2\x6b\x39\xc0\x12\x3e\x22\x24\xcf\xa8\x42\x84\x82\xc8\x28\xaa\x42\x36\xdd\x04\x00\xa0\xb9\xcc\xba\xda\x23\xd6\x92\x0c\x02\x63\x21\xb2\xaa\x3e\x72\x3a\xe7\x21\x1d\x99\xc6\x47\x9a\xb3\xa5\x16\x62\x26\x01\x09\x7c\xb8\x10\x9f\x55\x88\xd0\x18\xd9\x01\x55\xe2\xc4\x19\xe9\xec\xe5\x97\x33\x04\xa0\xc7\x6c\x0c\x6b\x8b\xc5\x00\xf4\x48\x04\xa3\xd5\xe0\x0d\x71\x91\x4a\xc3\x7f\xe6\x4c\x82\x2d\x18\x8b\x41\x7d\x69\x64\xd5\x90\xc9\x80\xf1\x30\x30\x7e\x93\xa4\xb4\xae\x76\x20\x9a\x72\xda\x80\xdc\x33\x51\xa1\xe6\xb0\xc3\xde\xda\xb2\x21\x69\x87\xe5\xe9\x02\xd4\x79\x68\x2a\x0c\x54\x66\x25\xa1\x8f\xeb\x9b\x38\x3c\x2b\x81\xb2\x03\x93\xea\x92\x64\x90\x1e\x00\x95\xea\x80\x0f\x80\x1f\x67\xd0\x75\x28\x03\x9b\x49\x15\x63\xe4\xa4\x42\x45\x18\x88\xe3\x10\xc8\xc5\x49\x2f\x5d\x55\xf8\x1a\x59\x56\x14\x0d\xa8\xcf\xe2\x26\x45\x54\xbf\x1c\x2a\x3f\x72\x41\x8e\x20\x62\x6b\x09\xc6\xcf\xb5\x9f\xbd\x18\xae\x85\x92\x70\x21\x6c\x80\x7e\xb9\xeb\xea\xe0\x19\xaf\x6a\x7f\x2b\xcb\x74\x6b\x97\x0b\x89\x95\xf7\x5d\x5f\x2c\x0e\xe3\x0a\x0a\x43\x2b\x15\xc2\x4d\xdd\x20\x40\x7a\xf4\x7a\x06\xed\x1b\xad\xac\x62\xb9\xcf\x40\x27\xc0\xb6\x6c\xbc\x91\x7a\x71\x22\x4c\x2e\x2a\x65\xa3\x54\x17\xec\x65\x02\xdc\xf8\x8e\x17\x7b\xcc\xf3\x71\xc3\x26\xd6\xc2\xa1\x92\x20\x09\x61\x67\xf7\x0a\xd2\xa0\x46\xf8\xf1\x53\xa4\x4d\xb8\x70\xff\x33\x8b\xeb\x41\xf5\xd2\x8c\xaf\x91\x03\xa4\xce\x47\x69\xdd\xcd\x75\x90\x6d\xc0\xdb\x5e\xb9\x84\xe7\x24\xf7\xc7\x8a\x36\x42\xe8\x0e\xc6\x99\x50\x13\xef\x5a\x9e\xee\x7d\x16\xac\x8b\x6b\x88\x53\xe7\x84\xdf\x6d\x3e\x91\xc6\x44\xdd\x51\x48\x53\xff\x0b\xfa\x47\xd8\x33\xd2\x99\x28\xe5\x80\x56\x48\x14\x60\xb6\x21\x84\x2a\x28\x40\x78\x10\x78\xbd\x2b\x89\x3c\x40\xbe\x04\x47\x30\xc5\x30\x2b\xe3\x1c\xc3\xd9\x09\xc7\x3b\x83\x49\x70\x7b\x75\x6d\xc6\x05\x39\x92\x12\x0a\x8b\xe3\x1d\x63\x25\x20\x6a\x24\x0a\x01\x28\xcf\x18\x2d\x4f\x11\x90\x52\x21\x11\x6c\xff\x24\xe0\x5a\x10\x75\xca\x18\x57\xab\x57\x85\xf2\x50\x65\x92\x7c\x07\xd3\xf7\xce\x56\xdf\x13\xda\x5a\x07\xb2\xae\xd6\x92\xd7\x72\x3f\x9f\xd9\xbe\x92\xdb\x48\x56\x0b\x7c\x9d\xe3\xcc\xc2\xd7\x66\x90\x9b\x07\x2e\x96\x00\x4f\x1c\xbe\x57\x61\xa8\x86\x9a\x75\x15\x67\xa0\x96\x27\x89\xd5\x65\xb5\xb5\x54\x39\xa1\x19\xe3\x40\x83\xbe\x21\x15\xe3\x59\x21\x10\x86\x8c\x83\x20\xcc\x29\x0a\x23\xc0\xe6\xb5\x40\xcd\xfe\x53\x32\x92\x14\x14\xb9\xe3\x8e\x06\xaa\x2a\xbe\xbf\xf0\x12\x40\xa9\xb0\xb3\xd7\x25\xa9\x88\xdf\x69\x1c\x56\x1b\x51\xaf\x75\xb5\x9a\xbb\x44\x9b\x29\xcf\xa2\x42\xf6\x4c\x87\x30\xdf\x20\x44\x74\x06\x07\x24\x16\xa4\x8e\xd6\x31\xf7\x9e\xfc\xe4\xea\x1b\x9c\xe7\x32\x86\x64\x2d\xbd\x9b\xfe\x20\x5b\x27\xfc\xa2\xd2\xcb\x3e\xc6\xd6\x5b\xfd\xb8\x9d\xaa\x96\xc1\x26\xae\x85\xa1\x72\xae\x01\x19\x41\xa7\xd3\x9e\xe4\x2f\x11\xa1\x0d\x1d\xb5\xe0\x0e\xdd\x44\xc4\xf1\x7e\xa7\xc8\xd8\xf9\xda\x51\x3f\xba\x22\xd0\x70\x30\xa3\x92\x48\x05\x14\x9f\xe2\x37\xda\x91\xc9\x2d\xf1\x54\x28\xf3\x7d\x57\x5c\xd7\xd5\x42\xa1\xa2\x8b\xb7\xd1\x8d\x4e\xbc\xaf\xf6\x83\xc0\x9f\xc2\x0a\x65\x98\x71\x8f\x6a\xe2\xd9\x58\x9a\x66\xad\xab\x8b\x99\x3a\xd4\x17\x32\x9e\x98\x78\x6c\x12\x52\x4e\xdc\x91\x63\x63\xa1\x2c\x98\x9d\x5a\x77\x7d\x03\x01\xd7\x50\x50\x07\x0d\x0e\x51\xe7\x07\x94\x3d\x90\x77\x78\x48\x24\xda\x59\x63\x33\x57\xa2\x6d\x32\x83\x38\x86\xf3\xbd\x00\x25\x88\x35\x4a\x18\x8a\x26\x3d\xb7\x83\xfc\x35\x2f\xdc\x15\xa9\x80\xd5\xee\x30\xb4\xd1\x0d\xa7\x47\x4a\xb5\xe1\x6a\x40\xa9\x1a\xa4\xad\xd3\x87\x51\xa9\x43\x5f\x1e\x54\x5c\x4c\xc2\x02\x9a\xb7\xa3\x8d\xa8\xec\x26\x80\x97\x04\x23\x19\xaa\x20\xae\xb8\x05\xae\x79\x8e\x14\x64\xdd\x4b\x9c\x45\x35\xdb\x4c\xb1\xc6\x91\x40\x65\x09\x25\x91\x55\x4c\xf1\x93\xe6\x50\x22\x67\xf4\x0f\xd6\xbd\x2d\xfa\x1e\x91\xb2\x16\x90\x21\xec\x0d\xd3\x16\x46\xc5\x28\x51\xcc\x19\x4e\xe2\xb6\xac\xd0\x73\x36\x6c\xdb\x82\x84\x5a\x12\xb3\x1b\x8f\xbd\xc0\xd5\x2c\xa1\xcb\xdd\xcb\xca\xea\x19\x15\x9d\x8b\x74\x8f\xc5\x0c\x3b\x4e\x58\x17\x20\x9b\xb0\x33\xde\xaf\x07\xf1\x83\x01\xbe\xbf\x1e\xc8\x38\x2b\x49\x57\x05\xac\xc1\x21\x66\xb4\x13\x72\x8c\x41\x5c\x69\x81\x8d\x39\x34\x3d\x4c\xc5\x55\xd0\x59\x5b\x84\x27\x42\x73\xf6\xb4\x60\xc3\xf5\x4c\x89\x97\x08\x83\x15\x1c\xaf\x15\xb4\x54\x02\x11\xaa\x16\x8f\x93\xae\x65\xeb\x8a\xd4\x3f\xda\x67\x20\x45\x8c\x70\xc1\xa4\xef\x4b\x0b\x98\xd7\xc1\xa1\x4b\x05\x15\x13\xee\x02\xf8\x0a\x1e\x87\x37\x73\x01\x16\x07\xb0\x15\x52\x60\xd4\x94\xae\x87\xca\x18\x5f\xff\x9a\x20\x3c\x89\xdb\x86\x03\x12\xe1\xa8\x5a\xcb\x3b\xa2\xe7\x96\xa9\x33\x07\x27\xf3\xed\x6c\xe2\x6f\x69\x43\xa7\x0e\x9f\xbd\x87\x90\xf5\x8e\x7a\xba\xc0\x69\x33\xb0\xe6\x6d\xf6\x8a\x41\x6f\x78\x72\xe0\xd1\xea\xc3\x58\x60\xdf\x8c\xb2\xda\x46\xab\xd8\x3b\xef\x5f\xef\xfc\x6d\xad\x6f\xdf\xed\xb9\x9a\x02\xa4\x14\xc2\x87\xa8\xfe\x61\x61\xd1\x78\x45\x1c\x9a\x74\xb9\xce\x30\xd4\x43\xfd\x13\x85\xfe\x26\x36\xfb\xf3\xec\xab\x7f\x5d\x1c\x7c\xd6\xdb\x42\x5d\x9c\xc7\x23\xde\xb2\xfe\x02\x3a\x7b\x6b\x55\x98\xc3\x03\x4d\x25\xd3\xbb\x84\x39\x49\x2e\x7d\x7d\xbb\x35\x8f\x61\x83\x39\xfe\x01\xc4\x4c\xa6\x73\xa3\xc5\x01\xc4\x73\x77\x65\x6d\xda\x0b\x71\x9e\xf3\x15\x83\xcd\xdd\x87\x99\x92\x61\xee\x25\xd2\x2b\xe5\xda\x15\xc6\xb6\x6e\x9d\x5a\x7d\xc6\x20\xdd\xe9\xa3\x7e\x8f\xff\x6b\xf8\x93\x27\xfe\x0d\x9f\xf4\x34\xb9\xeb\xfa\x61\x5e\xd4\x77\xcf\xf3\xb7\x86\x7c\x2c\x90\xee\x5d\x9f\x16\xdd\xb7\x7a\xeb\xe5\x53\xa3\xf3\xe1\xbf\x3d\x26\x18\x1e\xe0\x7b\x26\x97\x1b\xfd\x67\xfb\xcf\x12\x9b\x97\xcd\x9f\x01\x00\x00\xff\xff\x0c\x18\x50\x44\xa5\x35\x00\x00") func dataConfig_schema_v32JsonBytes() ([]byte, error) { return bindataRead( diff --git a/cli/compose/schema/data/config_schema_v3.2.json b/cli/compose/schema/data/config_schema_v3.2.json index a5bd3686fa..1a290476e1 100644 --- a/cli/compose/schema/data/config_schema_v3.2.json +++ b/cli/compose/schema/data/config_schema_v3.2.json @@ -72,6 +72,7 @@ "context": {"type": "string"}, "dockerfile": {"type": "string"}, "args": {"$ref": "#/definitions/list_or_dict"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, "cache_from": {"$ref": "#/definitions/list_of_strings"} }, "additionalProperties": false 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 +}