diff --git a/components/cli/cli/command/cli.go b/components/cli/cli/command/cli.go index e56c7a7985..e9b274f5ec 100644 --- a/components/cli/cli/command/cli.go +++ b/components/cli/cli/command/cli.go @@ -1,7 +1,6 @@ package command import ( - "fmt" "io" "net/http" "os" @@ -10,11 +9,9 @@ import ( "github.com/docker/cli/cli" cliconfig "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" - "github.com/docker/cli/cli/config/credentials" cliflags "github.com/docker/cli/cli/flags" dopts "github.com/docker/cli/opts" "github.com/docker/docker/api" - "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/docker/go-connections/sockets" "github.com/docker/go-connections/tlsconfig" @@ -39,7 +36,7 @@ type Cli interface { In() *InStream SetIn(in *InStream) ConfigFile() *configfile.ConfigFile - CredentialsStore(serverAddress string) credentials.Store + ServerInfo() ServerInfo } // DockerCli is an instance the docker command line client. @@ -104,59 +101,10 @@ func (cli *DockerCli) ServerInfo() ServerInfo { return cli.server } -// GetAllCredentials returns all of the credentials stored in all of the -// configured credential stores. -func (cli *DockerCli) GetAllCredentials() (map[string]types.AuthConfig, error) { - auths := make(map[string]types.AuthConfig) - for registry := range cli.configFile.CredentialHelpers { - helper := cli.CredentialsStore(registry) - newAuths, err := helper.GetAll() - if err != nil { - return nil, err - } - addAll(auths, newAuths) - } - defaultStore := cli.CredentialsStore("") - newAuths, err := defaultStore.GetAll() - if err != nil { - return nil, err - } - addAll(auths, newAuths) - return auths, nil -} - -func addAll(to, from map[string]types.AuthConfig) { - for reg, ac := range from { - to[reg] = ac - } -} - -// CredentialsStore returns a new credentials store based -// on the settings provided in the configuration file. Empty string returns -// the default credential store. -func (cli *DockerCli) CredentialsStore(serverAddress string) credentials.Store { - if helper := getConfiguredCredentialStore(cli.configFile, serverAddress); helper != "" { - return credentials.NewNativeStore(cli.configFile, helper) - } - return credentials.NewFileStore(cli.configFile) -} - -// getConfiguredCredentialStore returns the credential helper configured for the -// given registry, the default credsStore, or the empty string if neither are -// configured. -func getConfiguredCredentialStore(c *configfile.ConfigFile, serverAddress string) string { - if c.CredentialHelpers != nil && serverAddress != "" { - if helper, exists := c.CredentialHelpers[serverAddress]; exists { - return helper - } - } - return c.CredentialsStore -} - // Initialize the dockerCli runs initialization that must happen after command // line flags are parsed. func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { - cli.configFile = LoadDefaultConfigFile(cli.err) + cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err) var err error cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile) @@ -213,19 +161,6 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli { return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err} } -// LoadDefaultConfigFile attempts to load the default config file and returns -// an initialized ConfigFile struct if none is found. -func LoadDefaultConfigFile(err io.Writer) *configfile.ConfigFile { - configFile, e := cliconfig.Load(cliconfig.Dir()) - if e != nil { - fmt.Fprintf(err, "WARNING: Error loading config file:%v\n", e) - } - if !configFile.ContainsAuth() { - credentials.DetectDefaultStore(configFile) - } - return configFile -} - // NewAPIClientFromFlags creates a new APIClient from command line flags func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) { host, err := getServerHost(opts.Hosts, opts.TLSOptions) diff --git a/components/cli/cli/command/image/build.go b/components/cli/cli/command/image/build.go index a05970b8b0..d67ddfe9ff 100644 --- a/components/cli/cli/command/image/build.go +++ b/components/cli/cli/command/image/build.go @@ -77,16 +77,20 @@ func (o buildOptions) contextFromStdin() bool { return o.context == "-" } -// NewBuildCommand creates a new `docker build` command -func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { +func newBuildOptions() buildOptions { ulimits := make(map[string]*units.Ulimit) - options := buildOptions{ + return buildOptions{ tags: opts.NewListOpts(validateTag), buildArgs: opts.NewListOpts(opts.ValidateEnv), ulimits: opts.NewUlimitOpt(&ulimits), labels: opts.NewListOpts(opts.ValidateEnv), extraHosts: opts.NewListOpts(opts.ValidateExtraHost), } +} + +// NewBuildCommand creates a new `docker build` command +func NewBuildCommand(dockerCli command.Cli) *cobra.Command { + options := newBuildOptions() cmd := &cobra.Command{ Use: "build [OPTIONS] PATH | URL | -", @@ -159,7 +163,7 @@ func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error { } // nolint: gocyclo -func runBuild(dockerCli *command.DockerCli, options buildOptions) error { +func runBuild(dockerCli command.Cli, options buildOptions) error { var ( buildCtx io.ReadCloser dockerfileCtx io.ReadCloser @@ -237,13 +241,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { } excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, options.dockerfileFromStdin()) - - compression := archive.Uncompressed - if options.compress { - compression = archive.Gzip - } buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ - Compression: compression, ExcludePatterns: excludes, }) if err != nil { @@ -292,6 +290,13 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { } } + if options.compress { + buildCtx, err = build.Compress(buildCtx) + if err != nil { + return err + } + } + // Setup an upload progress bar progressOutput := streamformatter.NewProgressOutput(progBuff) if !dockerCli.Out().IsTerminal() { @@ -336,7 +341,8 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { body = buildCtx } - authConfigs, _ := dockerCli.GetAllCredentials() + configFile := dockerCli.ConfigFile() + authConfigs, _ := configFile.GetAllCredentials() buildOptions := types.ImageBuildOptions{ Memory: options.memory.Value(), MemorySwap: options.memorySwap.Value(), @@ -356,7 +362,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { Dockerfile: relDockerfile, ShmSize: options.shmSize.Value(), Ulimits: options.ulimits.GetList(), - BuildArgs: dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), options.buildArgs.GetAll()), + BuildArgs: configFile.ParseProxyConfig(dockerCli.Client().DaemonHost(), options.buildArgs.GetAll()), AuthConfigs: authConfigs, Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()), CacheFrom: options.cacheFrom, diff --git a/components/cli/cli/command/image/build/context.go b/components/cli/cli/command/image/build/context.go index b1457bdcc2..a98cd7b237 100644 --- a/components/cli/cli/command/image/build/context.go +++ b/components/cli/cli/command/image/build/context.go @@ -19,6 +19,7 @@ import ( "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/fileutils" "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/pools" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" "github.com/docker/docker/pkg/stringid" @@ -375,3 +376,27 @@ func AddDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCl }) return buildCtx, randomName, nil } + +// Compress the build context for sending to the API +func Compress(buildCtx io.ReadCloser) (io.ReadCloser, error) { + pipeReader, pipeWriter := io.Pipe() + + go func() { + compressWriter, err := archive.CompressStream(pipeWriter, archive.Gzip) + if err != nil { + pipeWriter.CloseWithError(err) + } + defer buildCtx.Close() + + if _, err := pools.Copy(compressWriter, buildCtx); err != nil { + pipeWriter.CloseWithError( + errors.Wrap(err, "failed to compress context")) + compressWriter.Close() + return + } + compressWriter.Close() + pipeWriter.Close() + }() + + return pipeReader, nil +} diff --git a/components/cli/cli/command/image/build_session.go b/components/cli/cli/command/image/build_session.go index c010c0ea0b..86fe95782e 100644 --- a/components/cli/cli/command/image/build_session.go +++ b/components/cli/cli/command/image/build_session.go @@ -26,11 +26,11 @@ import ( const clientSessionRemote = "client-session" -func isSessionSupported(dockerCli *command.DockerCli) bool { +func isSessionSupported(dockerCli command.Cli) bool { return dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31") } -func trySession(dockerCli *command.DockerCli, contextDir string) (*session.Session, error) { +func trySession(dockerCli command.Cli, contextDir string) (*session.Session, error) { var s *session.Session if isSessionSupported(dockerCli) { sharedKey, err := getBuildSharedKey(contextDir) diff --git a/components/cli/cli/command/image/build_test.go b/components/cli/cli/command/image/build_test.go new file mode 100644 index 0000000000..bf77292a85 --- /dev/null +++ b/components/cli/cli/command/image/build_test.go @@ -0,0 +1,70 @@ +package image + +import ( + "bytes" + "io" + "io/ioutil" + "os" + "path/filepath" + "sort" + "testing" + + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/internal/test" + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/archive" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" +) + +func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) { + dest, err := ioutil.TempDir("", "test-build-compress-dest") + require.NoError(t, err) + defer os.RemoveAll(dest) + + var dockerfileName string + fakeImageBuild := func(_ context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { + buffer := new(bytes.Buffer) + tee := io.TeeReader(context, buffer) + + assert.NoError(t, archive.Untar(tee, dest, nil)) + dockerfileName = options.Dockerfile + + header := buffer.Bytes()[:10] + assert.Equal(t, archive.Gzip, archive.DetectCompression(header)) + + body := new(bytes.Buffer) + return types.ImageBuildResponse{Body: ioutil.NopCloser(body)}, nil + } + + cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeImageBuild}, ioutil.Discard) + dockerfile := bytes.NewBufferString(` + FROM alpine:3.6 + COPY foo / + `) + cli.SetIn(command.NewInStream(ioutil.NopCloser(dockerfile))) + + dir, err := ioutil.TempDir("", "test-build-compress") + require.NoError(t, err) + defer os.RemoveAll(dir) + + ioutil.WriteFile(filepath.Join(dir, "foo"), []byte("some content"), 0644) + + options := newBuildOptions() + options.compress = true + options.dockerfileName = "-" + options.context = dir + + err = runBuild(cli, options) + require.NoError(t, err) + + files, err := ioutil.ReadDir(dest) + require.NoError(t, err) + actual := []string{} + for _, fileInfo := range files { + actual = append(actual, fileInfo.Name()) + } + sort.Strings(actual) + assert.Equal(t, []string{dockerfileName, ".dockerignore", "foo"}, actual) +} diff --git a/components/cli/cli/command/image/client_test.go b/components/cli/cli/command/image/client_test.go index 0df6fa4f77..b91eb7bd84 100644 --- a/components/cli/cli/command/image/client_test.go +++ b/components/cli/cli/command/image/client_test.go @@ -27,6 +27,7 @@ type fakeClient struct { 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) + imageBuildFunc func(context.Context, io.Reader, types.ImageBuildOptions) (types.ImageBuildResponse, error) } func (cli *fakeClient) ImageTag(_ context.Context, image, ref string) error { @@ -114,3 +115,10 @@ func (cli *fakeClient) ImageHistory(_ context.Context, img string) ([]image.Hist } return []image.HistoryResponseItem{{ID: img, Created: time.Now().Unix()}}, nil } + +func (cli *fakeClient) ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { + if cli.imageBuildFunc != nil { + return cli.imageBuildFunc(ctx, context, options) + } + return types.ImageBuildResponse{Body: ioutil.NopCloser(strings.NewReader(""))}, nil +} diff --git a/components/cli/cli/command/image/cmd.go b/components/cli/cli/command/image/cmd.go index 10357fcfd8..a12bf3395b 100644 --- a/components/cli/cli/command/image/cmd.go +++ b/components/cli/cli/command/image/cmd.go @@ -8,8 +8,7 @@ import ( ) // NewImageCommand returns a cobra command for `image` subcommands -// nolint: interfacer -func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command { +func NewImageCommand(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "image", Short: "Manage images", diff --git a/components/cli/cli/command/image/pull_test.go b/components/cli/cli/command/image/pull_test.go index d72531b768..690e6222eb 100644 --- a/components/cli/cli/command/image/pull_test.go +++ b/components/cli/cli/command/image/pull_test.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "testing" + "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/internal/test" "github.com/docker/docker/pkg/testutil" "github.com/docker/docker/pkg/testutil/golden" @@ -41,7 +42,9 @@ func TestNewPullCommandErrors(t *testing.T) { } for _, tc := range testCases { buf := new(bytes.Buffer) - cmd := NewPullCommand(test.NewFakeCli(&fakeClient{}, buf)) + cli := test.NewFakeCli(&fakeClient{}, buf) + cli.SetConfigfile(configfile.New("filename")) + cmd := NewPullCommand(cli) cmd.SetOutput(ioutil.Discard) cmd.SetArgs(tc.args) testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) @@ -64,7 +67,9 @@ func TestNewPullCommandSuccess(t *testing.T) { } for _, tc := range testCases { buf := new(bytes.Buffer) - cmd := NewPullCommand(test.NewFakeCli(&fakeClient{}, buf)) + cli := test.NewFakeCli(&fakeClient{}, buf) + cli.SetConfigfile(configfile.New("filename")) + cmd := NewPullCommand(cli) cmd.SetOutput(ioutil.Discard) cmd.SetArgs(tc.args) err := cmd.Execute() diff --git a/components/cli/cli/command/image/push_test.go b/components/cli/cli/command/image/push_test.go index 559b1b89c3..f2c35dee21 100644 --- a/components/cli/cli/command/image/push_test.go +++ b/components/cli/cli/command/image/push_test.go @@ -7,6 +7,7 @@ import ( "strings" "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" @@ -47,7 +48,9 @@ func TestNewPushCommandErrors(t *testing.T) { } for _, tc := range testCases { buf := new(bytes.Buffer) - cmd := NewPushCommand(test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc}, buf)) + cli := test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc}, buf) + cli.SetConfigfile(configfile.New("filename")) + cmd := NewPushCommand(cli) cmd.SetOutput(ioutil.Discard) cmd.SetArgs(tc.args) testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) @@ -66,11 +69,13 @@ func TestNewPushCommandSuccess(t *testing.T) { } for _, tc := range testCases { buf := new(bytes.Buffer) - cmd := NewPushCommand(test.NewFakeCli(&fakeClient{ + cli := test.NewFakeCli(&fakeClient{ imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) { return ioutil.NopCloser(strings.NewReader("")), nil }, - }, buf)) + }, buf) + cli.SetConfigfile(configfile.New("filename")) + cmd := NewPushCommand(cli) cmd.SetOutput(ioutil.Discard) cmd.SetArgs(tc.args) assert.NoError(t, cmd.Execute()) diff --git a/components/cli/cli/command/registry.go b/components/cli/cli/command/registry.go index 802b3a4b83..f3958d8a46 100644 --- a/components/cli/cli/command/registry.go +++ b/components/cli/cli/command/registry.go @@ -70,7 +70,7 @@ func ResolveAuthConfig(ctx context.Context, cli Cli, index *registrytypes.IndexI configKey = ElectAuthServer(ctx, cli) } - a, _ := cli.CredentialsStore(configKey).Get(configKey) + a, _ := cli.ConfigFile().GetAuthConfig(configKey) return a } @@ -85,7 +85,7 @@ func ConfigureAuth(cli Cli, flUser, flPassword, serverAddress string, isDefaultR serverAddress = registry.ConvertToHostname(serverAddress) } - authconfig, err := cli.CredentialsStore(serverAddress).Get(serverAddress) + authconfig, err := cli.ConfigFile().GetAuthConfig(serverAddress) if err != nil { return authconfig, err } diff --git a/components/cli/cli/command/registry/login.go b/components/cli/cli/command/registry/login.go index ba1b133054..4ffca2bed2 100644 --- a/components/cli/cli/command/registry/login.go +++ b/components/cli/cli/command/registry/login.go @@ -71,7 +71,7 @@ func runLogin(dockerCli command.Cli, opts loginOptions) error { authConfig.Password = "" authConfig.IdentityToken = response.IdentityToken } - if err := dockerCli.CredentialsStore(serverAddress).Store(authConfig); err != nil { + if err := dockerCli.ConfigFile().GetCredentialsStore(serverAddress).Store(authConfig); err != nil { return errors.Errorf("Error saving credentials: %v", err) } diff --git a/components/cli/cli/command/registry/logout.go b/components/cli/cli/command/registry/logout.go index d241df4cf0..cce626e58a 100644 --- a/components/cli/cli/command/registry/logout.go +++ b/components/cli/cli/command/registry/logout.go @@ -68,7 +68,7 @@ func runLogout(dockerCli command.Cli, serverAddress string) error { fmt.Fprintf(dockerCli.Out(), "Removing login credentials for %s\n", hostnameAddress) for _, r := range regsToLogout { - if err := dockerCli.CredentialsStore(r).Erase(r); err != nil { + if err := dockerCli.ConfigFile().GetCredentialsStore(r).Erase(r); err != nil { fmt.Fprintf(dockerCli.Err(), "WARNING: could not erase credentials: %v\n", err) } } diff --git a/components/cli/cli/command/service/progress/progress.go b/components/cli/cli/command/service/progress/progress.go index d8300ce8d2..d522fc08b4 100644 --- a/components/cli/cli/command/service/progress/progress.go +++ b/components/cli/cli/command/service/progress/progress.go @@ -129,6 +129,11 @@ func ServiceProgress(ctx context.Context, client client.APIClient, serviceID str } } if converged && time.Since(convergedAt) >= monitor { + progressOut.WriteProgress(progress.Progress{ + ID: "verify", + Action: "Service converged", + }) + return nil } diff --git a/components/cli/cli/command/swarm/ca.go b/components/cli/cli/command/swarm/ca.go index 2f01ab4da4..83ac02e776 100644 --- a/components/cli/cli/command/swarm/ca.go +++ b/components/cli/cli/command/swarm/ca.go @@ -3,23 +3,22 @@ package swarm import ( "fmt" "io" - "strings" - - "golang.org/x/net/context" - "io/ioutil" + "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/swarm/progress" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/pkg/jsonmessage" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" + "golang.org/x/net/context" ) type caOptions struct { - swarmOptions + swarmCAOptions rootCACert PEMFile rootCAKey PEMFile rotate bool @@ -27,21 +26,21 @@ type caOptions struct { quiet bool } -func newRotateCACommand(dockerCli command.Cli) *cobra.Command { +func newCACommand(dockerCli command.Cli) *cobra.Command { opts := caOptions{} cmd := &cobra.Command{ Use: "ca [OPTIONS]", - Short: "Manage root CA", + Short: "Display and rotate the root CA", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runRotateCA(dockerCli, cmd.Flags(), opts) + return runCA(dockerCli, cmd.Flags(), opts) }, Tags: map[string]string{"version": "1.30"}, } flags := cmd.Flags() - addSwarmCAFlags(flags, &opts.swarmOptions) + addSwarmCAFlags(flags, &opts.swarmCAOptions) flags.BoolVar(&opts.rotate, flagRotate, false, "Rotate the swarm CA - if no certificate or key are provided, new ones will be generated") flags.Var(&opts.rootCACert, flagCACert, "Path to the PEM-formatted root CA certificate to use for the new cluster") flags.Var(&opts.rootCAKey, flagCAKey, "Path to the PEM-formatted root CA key to use for the new cluster") @@ -51,7 +50,7 @@ func newRotateCACommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runRotateCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) error { +func runCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) error { client := dockerCli.Client() ctx := context.Background() @@ -66,31 +65,10 @@ func runRotateCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) er return fmt.Errorf("`--%s` flag requires the `--rotate` flag to update the CA", f) } } - if swarmInspect.ClusterInfo.TLSInfo.TrustRoot == "" { - fmt.Fprintln(dockerCli.Out(), "No CA information available") - } else { - fmt.Fprintln(dockerCli.Out(), strings.TrimSpace(swarmInspect.ClusterInfo.TLSInfo.TrustRoot)) - } - return nil - } - - genRootCA := true - spec := &swarmInspect.Spec - opts.mergeSwarmSpec(spec, flags) // updates the spec given the cert expiry or external CA flag - if flags.Changed(flagCACert) { - spec.CAConfig.SigningCACert = opts.rootCACert.Contents() - genRootCA = false - } - if flags.Changed(flagCAKey) { - spec.CAConfig.SigningCAKey = opts.rootCAKey.Contents() - genRootCA = false - } - if genRootCA { - spec.CAConfig.ForceRotate++ - spec.CAConfig.SigningCACert = "" - spec.CAConfig.SigningCAKey = "" + return displayTrustRoot(dockerCli.Out(), swarmInspect) } + updateSwarmSpec(&swarmInspect.Spec, flags, opts) if err := client.SwarmUpdate(ctx, swarmInspect.Version, swarmInspect.Spec, swarm.UpdateFlags{}); err != nil { return err } @@ -98,7 +76,29 @@ func runRotateCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) er if opts.detach { return nil } + return attach(ctx, dockerCli, opts) +} +func updateSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet, opts caOptions) { + opts.mergeSwarmSpecCAFlags(spec, flags) + caCert := opts.rootCACert.Contents() + caKey := opts.rootCAKey.Contents() + + if caCert != "" { + spec.CAConfig.SigningCACert = caCert + } + if caKey != "" { + spec.CAConfig.SigningCAKey = caKey + } + if caKey == "" && caCert == "" { + spec.CAConfig.ForceRotate++ + spec.CAConfig.SigningCACert = "" + spec.CAConfig.SigningCAKey = "" + } +} + +func attach(ctx context.Context, dockerCli command.Cli, opts caOptions) error { + client := dockerCli.Client() errChan := make(chan error, 1) pipeReader, pipeWriter := io.Pipe() @@ -111,7 +111,7 @@ func runRotateCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) er return <-errChan } - err = jsonmessage.DisplayJSONMessagesToStream(pipeReader, dockerCli.Out(), nil) + err := jsonmessage.DisplayJSONMessagesToStream(pipeReader, dockerCli.Out(), nil) if err == nil { err = <-errChan } @@ -119,15 +119,17 @@ func runRotateCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) er return err } - swarmInspect, err = client.SwarmInspect(ctx) + swarmInspect, err := client.SwarmInspect(ctx) if err != nil { return err } + return displayTrustRoot(dockerCli.Out(), swarmInspect) +} - if swarmInspect.ClusterInfo.TLSInfo.TrustRoot == "" { - fmt.Fprintln(dockerCli.Out(), "No CA information available") - } else { - fmt.Fprintln(dockerCli.Out(), strings.TrimSpace(swarmInspect.ClusterInfo.TLSInfo.TrustRoot)) +func displayTrustRoot(out io.Writer, info swarm.Swarm) error { + if info.ClusterInfo.TLSInfo.TrustRoot == "" { + return errors.New("No CA information available") } + fmt.Fprintln(out, strings.TrimSpace(info.ClusterInfo.TLSInfo.TrustRoot)) return nil } diff --git a/components/cli/cli/command/swarm/ca_test.go b/components/cli/cli/command/swarm/ca_test.go new file mode 100644 index 0000000000..f122567c47 --- /dev/null +++ b/components/cli/cli/command/swarm/ca_test.go @@ -0,0 +1,88 @@ +package swarm + +import ( + "bytes" + "testing" + "time" + + "github.com/docker/docker/api/types/swarm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func swarmSpecWithFullCAConfig() *swarm.Spec { + return &swarm.Spec{ + CAConfig: swarm.CAConfig{ + SigningCACert: "cacert", + SigningCAKey: "cakey", + ForceRotate: 1, + NodeCertExpiry: time.Duration(200), + ExternalCAs: []*swarm.ExternalCA{ + { + URL: "https://example.com/ca", + Protocol: swarm.ExternalCAProtocolCFSSL, + CACert: "excacert", + }, + }, + }, + } +} + +func TestDisplayTrustRootNoRoot(t *testing.T) { + buffer := new(bytes.Buffer) + err := displayTrustRoot(buffer, swarm.Swarm{}) + assert.EqualError(t, err, "No CA information available") +} + +func TestDisplayTrustRoot(t *testing.T) { + buffer := new(bytes.Buffer) + trustRoot := "trustme" + err := displayTrustRoot(buffer, swarm.Swarm{ + ClusterInfo: swarm.ClusterInfo{ + TLSInfo: swarm.TLSInfo{TrustRoot: trustRoot}, + }, + }) + require.NoError(t, err) + assert.Equal(t, trustRoot+"\n", buffer.String()) +} + +func TestUpdateSwarmSpecDefaultRotate(t *testing.T) { + spec := swarmSpecWithFullCAConfig() + flags := newCACommand(nil).Flags() + updateSwarmSpec(spec, flags, caOptions{}) + + expected := swarmSpecWithFullCAConfig() + expected.CAConfig.ForceRotate = 2 + expected.CAConfig.SigningCACert = "" + expected.CAConfig.SigningCAKey = "" + assert.Equal(t, expected, spec) +} + +func TestUpdateSwarmSpecPartial(t *testing.T) { + spec := swarmSpecWithFullCAConfig() + flags := newCACommand(nil).Flags() + updateSwarmSpec(spec, flags, caOptions{ + rootCACert: PEMFile{contents: "cacert"}, + }) + + expected := swarmSpecWithFullCAConfig() + expected.CAConfig.SigningCACert = "cacert" + assert.Equal(t, expected, spec) +} + +func TestUpdateSwarmSpecFullFlags(t *testing.T) { + flags := newCACommand(nil).Flags() + flags.Lookup(flagCertExpiry).Changed = true + spec := swarmSpecWithFullCAConfig() + updateSwarmSpec(spec, flags, caOptions{ + rootCACert: PEMFile{contents: "cacert"}, + rootCAKey: PEMFile{contents: "cakey"}, + swarmCAOptions: swarmCAOptions{nodeCertExpiry: 3 * time.Minute}, + }) + + expected := swarmSpecWithFullCAConfig() + expected.CAConfig.SigningCACert = "cacert" + expected.CAConfig.SigningCAKey = "cakey" + expected.CAConfig.NodeCertExpiry = 3 * time.Minute + assert.Equal(t, expected, spec) +} diff --git a/components/cli/cli/command/swarm/cmd.go b/components/cli/cli/command/swarm/cmd.go index b7e6dcfda2..d2793f6b91 100644 --- a/components/cli/cli/command/swarm/cmd.go +++ b/components/cli/cli/command/swarm/cmd.go @@ -25,7 +25,7 @@ func NewSwarmCommand(dockerCli command.Cli) *cobra.Command { newUpdateCommand(dockerCli), newLeaveCommand(dockerCli), newUnlockCommand(dockerCli), - newRotateCACommand(dockerCli), + newCACommand(dockerCli), ) return cmd } diff --git a/components/cli/cli/command/swarm/opts.go b/components/cli/cli/command/swarm/opts.go index 0522f9fdfa..b3baba273e 100644 --- a/components/cli/cli/command/swarm/opts.go +++ b/components/cli/cli/command/swarm/opts.go @@ -36,10 +36,9 @@ const ( ) type swarmOptions struct { + swarmCAOptions taskHistoryLimit int64 dispatcherHeartbeat time.Duration - nodeCertExpiry time.Duration - externalCA ExternalCAOption maxSnapshots uint64 snapshotInterval uint64 autolock bool @@ -216,7 +215,7 @@ func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) { return &externalCA, nil } -func addSwarmCAFlags(flags *pflag.FlagSet, opts *swarmOptions) { +func addSwarmCAFlags(flags *pflag.FlagSet, opts *swarmCAOptions) { flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, 90*24*time.Hour, "Validity period for node certificates (ns|us|ms|s|m|h)") flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints") } @@ -228,7 +227,7 @@ func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) { flags.SetAnnotation(flagMaxSnapshots, "version", []string{"1.25"}) flags.Uint64Var(&opts.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots") flags.SetAnnotation(flagSnapshotInterval, "version", []string{"1.25"}) - addSwarmCAFlags(flags, opts) + addSwarmCAFlags(flags, &opts.swarmCAOptions) } func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) { @@ -238,12 +237,6 @@ func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) if flags.Changed(flagDispatcherHeartbeat) { spec.Dispatcher.HeartbeatPeriod = opts.dispatcherHeartbeat } - if flags.Changed(flagCertExpiry) { - spec.CAConfig.NodeCertExpiry = opts.nodeCertExpiry - } - if flags.Changed(flagExternalCA) { - spec.CAConfig.ExternalCAs = opts.externalCA.Value() - } if flags.Changed(flagMaxSnapshots) { spec.Raft.KeepOldSnapshots = &opts.maxSnapshots } @@ -253,6 +246,21 @@ func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) if flags.Changed(flagAutolock) { spec.EncryptionConfig.AutoLockManagers = opts.autolock } + opts.mergeSwarmSpecCAFlags(spec, flags) +} + +type swarmCAOptions struct { + nodeCertExpiry time.Duration + externalCA ExternalCAOption +} + +func (opts *swarmCAOptions) mergeSwarmSpecCAFlags(spec *swarm.Spec, flags *pflag.FlagSet) { + if flags.Changed(flagCertExpiry) { + spec.CAConfig.NodeCertExpiry = opts.nodeCertExpiry + } + if flags.Changed(flagExternalCA) { + spec.CAConfig.ExternalCAs = opts.externalCA.Value() + } } func (opts *swarmOptions) ToSpec(flags *pflag.FlagSet) swarm.Spec { diff --git a/components/cli/cli/command/system/prune.go b/components/cli/cli/command/system/prune.go index 737eac84ae..e9383e43d3 100644 --- a/components/cli/cli/command/system/prune.go +++ b/components/cli/cli/command/system/prune.go @@ -1,7 +1,9 @@ package system import ( + "bytes" "fmt" + "text/template" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -44,29 +46,14 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command { return cmd } -const ( - warning = `WARNING! This will remove: - - all stopped containers - - all volumes not used by at least one container - - all networks not used by at least one container - %s - - all build cache +const confirmationTemplate = `WARNING! This will remove: +{{- range $_, $warning := . }} + - {{ $warning }} +{{- end }} Are you sure you want to continue?` - danglingImageDesc = "- all dangling images" - allImageDesc = `- all images without at least one container associated to them` -) - func runPrune(dockerCli command.Cli, options pruneOptions) error { - var message string - - if options.all { - message = fmt.Sprintf(warning, allImageDesc) - } else { - message = fmt.Sprintf(warning, danglingImageDesc) - } - - if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), message) { + if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), confirmationMessage(options)) { return nil } @@ -109,3 +96,26 @@ func runPrune(dockerCli command.Cli, options pruneOptions) error { return nil } + +// confirmationMessage constructs a confirmation message that depends on the cli options. +func confirmationMessage(options pruneOptions) string { + t := template.Must(template.New("confirmation message").Parse(confirmationTemplate)) + + warnings := []string{ + "all stopped containers", + "all networks not used by at least one container", + } + if options.pruneVolumes { + warnings = append(warnings, "all volumes not used by at least one container") + } + if options.all { + warnings = append(warnings, "all images without at least one container associated to them") + } else { + warnings = append(warnings, "all dangling images") + } + warnings = append(warnings, "all build cache") + + var buffer bytes.Buffer + t.Execute(&buffer, &warnings) + return buffer.String() +} diff --git a/components/cli/cli/command/task/client_test.go b/components/cli/cli/command/task/client_test.go new file mode 100644 index 0000000000..d04405c236 --- /dev/null +++ b/components/cli/cli/command/task/client_test.go @@ -0,0 +1,28 @@ +package task + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" + "golang.org/x/net/context" +) + +type fakeClient struct { + client.APIClient + nodeInspectWithRaw func(ref string) (swarm.Node, []byte, error) + serviceInspectWithRaw func(ref string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) +} + +func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, ref string) (swarm.Node, []byte, error) { + if cli.nodeInspectWithRaw != nil { + return cli.nodeInspectWithRaw(ref) + } + return swarm.Node{}, nil, nil +} + +func (cli *fakeClient) ServiceInspectWithRaw(ctx context.Context, ref string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) { + if cli.serviceInspectWithRaw != nil { + return cli.serviceInspectWithRaw(ref, options) + } + return swarm.Service{}, nil, nil +} diff --git a/components/cli/cli/command/task/print_test.go b/components/cli/cli/command/task/print_test.go new file mode 100644 index 0000000000..93171016f2 --- /dev/null +++ b/components/cli/cli/command/task/print_test.go @@ -0,0 +1,150 @@ +package task + +import ( + "bytes" + "testing" + "time" + + "github.com/docker/cli/cli/command/formatter" + "github.com/docker/cli/cli/command/idresolver" + "github.com/docker/cli/cli/internal/test" + "golang.org/x/net/context" + // Import builders to get the builder function as package function + . "github.com/docker/cli/cli/internal/test/builders" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/pkg/testutil" + "github.com/docker/docker/pkg/testutil/golden" + "github.com/stretchr/testify/assert" +) + +func TestTaskPrintWithQuietOption(t *testing.T) { + quiet := true + trunc := false + noResolve := true + buf := new(bytes.Buffer) + apiClient := &fakeClient{} + cli := test.NewFakeCli(apiClient, buf) + tasks := []swarm.Task{ + *Task(TaskID("id-foo")), + } + err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, formatter.TableFormatKey) + assert.NoError(t, err) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "task-print-with-quiet-option.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} + +func TestTaskPrintWithNoTruncOption(t *testing.T) { + quiet := false + trunc := false + noResolve := true + buf := new(bytes.Buffer) + apiClient := &fakeClient{} + cli := test.NewFakeCli(apiClient, buf) + tasks := []swarm.Task{ + *Task(TaskID("id-foo-yov6omdek8fg3k5stosyp2m50")), + } + err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, "{{ .ID }}") + assert.NoError(t, err) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "task-print-with-no-trunc-option.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} + +func TestTaskPrintWithGlobalService(t *testing.T) { + quiet := false + trunc := false + noResolve := true + buf := new(bytes.Buffer) + apiClient := &fakeClient{} + cli := test.NewFakeCli(apiClient, buf) + tasks := []swarm.Task{ + *Task(TaskServiceID("service-id-foo"), TaskNodeID("node-id-bar"), TaskSlot(0)), + } + err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, "{{ .Name }}") + assert.NoError(t, err) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "task-print-with-global-service.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} + +func TestTaskPrintWithReplicatedService(t *testing.T) { + quiet := false + trunc := false + noResolve := true + buf := new(bytes.Buffer) + apiClient := &fakeClient{} + cli := test.NewFakeCli(apiClient, buf) + tasks := []swarm.Task{ + *Task(TaskServiceID("service-id-foo"), TaskSlot(1)), + } + err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, "{{ .Name }}") + assert.NoError(t, err) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "task-print-with-replicated-service.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} + +func TestTaskPrintWithIndentation(t *testing.T) { + quiet := false + trunc := false + noResolve := false + buf := new(bytes.Buffer) + apiClient := &fakeClient{ + serviceInspectWithRaw: func(ref string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) { + return *Service(ServiceName("service-name-foo")), nil, nil + }, + nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) { + return *Node(NodeName("node-name-bar")), nil, nil + }, + } + cli := test.NewFakeCli(apiClient, buf) + tasks := []swarm.Task{ + *Task( + TaskID("id-foo"), + TaskServiceID("service-id-foo"), + TaskNodeID("id-node"), + WithTaskSpec(TaskImage("myimage:mytag")), + TaskDesiredState(swarm.TaskStateReady), + WithStatus(TaskState(swarm.TaskStateFailed), Timestamp(time.Now().Add(-2*time.Hour))), + ), + *Task( + TaskID("id-bar"), + TaskServiceID("service-id-foo"), + TaskNodeID("id-node"), + WithTaskSpec(TaskImage("myimage:mytag")), + TaskDesiredState(swarm.TaskStateReady), + WithStatus(TaskState(swarm.TaskStateFailed), Timestamp(time.Now().Add(-2*time.Hour))), + ), + } + err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, formatter.TableFormatKey) + assert.NoError(t, err) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "task-print-with-indentation.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} + +func TestTaskPrintWithResolution(t *testing.T) { + quiet := false + trunc := false + noResolve := false + buf := new(bytes.Buffer) + apiClient := &fakeClient{ + serviceInspectWithRaw: func(ref string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) { + return *Service(ServiceName("service-name-foo")), nil, nil + }, + nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) { + return *Node(NodeName("node-name-bar")), nil, nil + }, + } + cli := test.NewFakeCli(apiClient, buf) + tasks := []swarm.Task{ + *Task(TaskServiceID("service-id-foo"), TaskSlot(1)), + } + err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, "{{ .Name }} {{ .Node }}") + assert.NoError(t, err) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "task-print-with-resolution.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} diff --git a/components/cli/cli/command/task/testdata/task-print-with-global-service.golden b/components/cli/cli/command/task/testdata/task-print-with-global-service.golden new file mode 100644 index 0000000000..fbc81248dd --- /dev/null +++ b/components/cli/cli/command/task/testdata/task-print-with-global-service.golden @@ -0,0 +1 @@ +service-id-foo.node-id-bar diff --git a/components/cli/cli/command/task/testdata/task-print-with-indentation.golden b/components/cli/cli/command/task/testdata/task-print-with-indentation.golden new file mode 100644 index 0000000000..932126ad94 --- /dev/null +++ b/components/cli/cli/command/task/testdata/task-print-with-indentation.golden @@ -0,0 +1,3 @@ +ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS +id-foo service-name-foo.1 myimage:mytag node-name-bar Ready Failed 2 hours ago +id-bar \_ service-name-foo.1 myimage:mytag node-name-bar Ready Failed 2 hours ago diff --git a/components/cli/cli/command/task/testdata/task-print-with-no-trunc-option.golden b/components/cli/cli/command/task/testdata/task-print-with-no-trunc-option.golden new file mode 100644 index 0000000000..184d2de201 --- /dev/null +++ b/components/cli/cli/command/task/testdata/task-print-with-no-trunc-option.golden @@ -0,0 +1 @@ +id-foo-yov6omdek8fg3k5stosyp2m50 diff --git a/components/cli/cli/command/task/testdata/task-print-with-quiet-option.golden b/components/cli/cli/command/task/testdata/task-print-with-quiet-option.golden new file mode 100644 index 0000000000..e2faeb6067 --- /dev/null +++ b/components/cli/cli/command/task/testdata/task-print-with-quiet-option.golden @@ -0,0 +1 @@ +id-foo diff --git a/components/cli/cli/command/task/testdata/task-print-with-replicated-service.golden b/components/cli/cli/command/task/testdata/task-print-with-replicated-service.golden new file mode 100644 index 0000000000..9ecebdafe3 --- /dev/null +++ b/components/cli/cli/command/task/testdata/task-print-with-replicated-service.golden @@ -0,0 +1 @@ +service-id-foo.1 diff --git a/components/cli/cli/command/task/testdata/task-print-with-resolution.golden b/components/cli/cli/command/task/testdata/task-print-with-resolution.golden new file mode 100644 index 0000000000..747d1af462 --- /dev/null +++ b/components/cli/cli/command/task/testdata/task-print-with-resolution.golden @@ -0,0 +1 @@ +service-name-foo.1 node-name-bar diff --git a/components/cli/cli/compose/loader/full-example.yml b/components/cli/cli/compose/loader/full-example.yml index e1e3b77d39..76087c57ba 100644 --- a/components/cli/cli/compose/loader/full-example.yml +++ b/components/cli/cli/compose/loader/full-example.yml @@ -39,7 +39,7 @@ services: cpus: '0.0001' memory: 20M restart_policy: - condition: on_failure + condition: on-failure delay: 5s max_attempts: 3 window: 120s diff --git a/components/cli/cli/compose/loader/loader_test.go b/components/cli/cli/compose/loader/loader_test.go index 43a02204b7..6a43e52121 100644 --- a/components/cli/cli/compose/loader/loader_test.go +++ b/components/cli/cli/compose/loader/loader_test.go @@ -683,7 +683,7 @@ func TestFullExample(t *testing.T) { }, }, RestartPolicy: &types.RestartPolicy{ - Condition: "on_failure", + Condition: "on-failure", Delay: durationPtr(5 * time.Second), MaxAttempts: uint64Ptr(3), Window: durationPtr(2 * time.Minute), diff --git a/components/cli/cli/config/config.go b/components/cli/cli/config/config.go index 58c83b6404..90529ebd4c 100644 --- a/components/cli/cli/config/config.go +++ b/components/cli/cli/config/config.go @@ -1,11 +1,13 @@ package config import ( + "fmt" "io" "os" "path/filepath" "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/config/credentials" "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/homedir" "github.com/pkg/errors" @@ -38,15 +40,6 @@ func SetDir(dir string) { configDir = dir } -// NewConfigFile initializes an empty configuration file for the given filename 'fn' -func NewConfigFile(fn string) *configfile.ConfigFile { - return &configfile.ConfigFile{ - AuthConfigs: make(map[string]types.AuthConfig), - HTTPHeaders: make(map[string]string), - Filename: fn, - } -} - // LegacyLoadFromReader is a convenience function that creates a ConfigFile object from // a non-nested reader func LegacyLoadFromReader(configData io.Reader) (*configfile.ConfigFile, error) { @@ -75,46 +68,53 @@ func Load(configDir string) (*configfile.ConfigFile, error) { configDir = Dir() } - configFile := configfile.ConfigFile{ - AuthConfigs: make(map[string]types.AuthConfig), - Filename: filepath.Join(configDir, ConfigFileName), - } + filename := filepath.Join(configDir, ConfigFileName) + configFile := configfile.New(filename) // Try happy path first - latest config file - if _, err := os.Stat(configFile.Filename); err == nil { - file, err := os.Open(configFile.Filename) + if _, err := os.Stat(filename); err == nil { + file, err := os.Open(filename) if err != nil { - return &configFile, errors.Errorf("%s - %v", configFile.Filename, err) + return configFile, errors.Errorf("%s - %v", filename, err) } defer file.Close() err = configFile.LoadFromReader(file) if err != nil { - err = errors.Errorf("%s - %v", configFile.Filename, err) + err = errors.Errorf("%s - %v", filename, err) } - return &configFile, err + return configFile, err } else if !os.IsNotExist(err) { // if file is there but we can't stat it for any reason other // than it doesn't exist then stop - return &configFile, errors.Errorf("%s - %v", configFile.Filename, err) + return configFile, errors.Errorf("%s - %v", filename, err) } // Can't find latest config file so check for the old one confFile := filepath.Join(homedir.Get(), oldConfigfile) if _, err := os.Stat(confFile); err != nil { - return &configFile, nil //missing file is not an error + return configFile, nil //missing file is not an error } file, err := os.Open(confFile) if err != nil { - return &configFile, errors.Errorf("%s - %v", confFile, err) + return configFile, errors.Errorf("%s - %v", confFile, err) } defer file.Close() err = configFile.LegacyLoadFromReader(file) if err != nil { - return &configFile, errors.Errorf("%s - %v", confFile, err) + return configFile, errors.Errorf("%s - %v", confFile, err) } - - if configFile.HTTPHeaders == nil { - configFile.HTTPHeaders = map[string]string{} - } - return &configFile, nil + return configFile, nil +} + +// LoadDefaultConfigFile attempts to load the default config file and returns +// an initialized ConfigFile struct if none is found. +func LoadDefaultConfigFile(stderr io.Writer) *configfile.ConfigFile { + configFile, err := Load(Dir()) + if err != nil { + fmt.Fprintf(stderr, "WARNING: Error loading config file: %v\n", err) + } + if !configFile.ContainsAuth() { + configFile.CredentialsStore = credentials.DetectDefaultStore(configFile.CredentialsStore) + } + return configFile } diff --git a/components/cli/cli/config/config_test.go b/components/cli/cli/config/config_test.go index 7eab2efaa3..c885d3e724 100644 --- a/components/cli/cli/config/config_test.go +++ b/components/cli/cli/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "bytes" "io/ioutil" "os" "path/filepath" @@ -8,28 +9,34 @@ import ( "testing" "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/config/credentials" "github.com/docker/docker/pkg/homedir" + "github.com/docker/docker/pkg/testutil" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestEmptyConfigDir(t *testing.T) { - tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpHome) +func setupConfigDir(t *testing.T) (string, func()) { + tmpdir, err := ioutil.TempDir("", "config-test") + require.NoError(t, err) + oldDir := Dir() + SetDir(tmpdir) - SetDir(tmpHome) + return tmpdir, func() { + SetDir(oldDir) + os.RemoveAll(tmpdir) + } +} + +func TestEmptyConfigDir(t *testing.T) { + tmpHome, cleanup := setupConfigDir(t) + defer cleanup() config, err := Load("") - if err != nil { - t.Fatalf("Failed loading on empty config dir: %q", err) - } + require.NoError(t, err) expectedConfigFilename := filepath.Join(tmpHome, ConfigFileName) - if config.Filename != expectedConfigFilename { - t.Fatalf("Expected config filename %s, got %s", expectedConfigFilename, config.Filename) - } + assert.Equal(t, expectedConfigFilename, config.Filename) // Now save it and make sure it shows up in new form saveConfigAndValidateNewFormat(t, config, tmpHome) @@ -37,15 +44,11 @@ func TestEmptyConfigDir(t *testing.T) { func TestMissingFile(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer os.RemoveAll(tmpHome) config, err := Load(tmpHome) - if err != nil { - t.Fatalf("Failed loading on missing file: %q", err) - } + require.NoError(t, err) // Now save it and make sure it shows up in new form saveConfigAndValidateNewFormat(t, config, tmpHome) @@ -53,17 +56,13 @@ func TestMissingFile(t *testing.T) { func TestSaveFileToDirs(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer os.RemoveAll(tmpHome) tmpHome += "/.docker" config, err := Load(tmpHome) - if err != nil { - t.Fatalf("Failed loading on missing file: %q", err) - } + require.NoError(t, err) // Now save it and make sure it shows up in new form saveConfigAndValidateNewFormat(t, config, tmpHome) @@ -71,38 +70,28 @@ func TestSaveFileToDirs(t *testing.T) { func TestEmptyFile(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer os.RemoveAll(tmpHome) fn := filepath.Join(tmpHome, ConfigFileName) - if err := ioutil.WriteFile(fn, []byte(""), 0600); err != nil { - t.Fatal(err) - } + err = ioutil.WriteFile(fn, []byte(""), 0600) + require.NoError(t, err) _, err = Load(tmpHome) - if err == nil { - t.Fatalf("Was supposed to fail") - } + testutil.ErrorContains(t, err, "EOF") } func TestEmptyJSON(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer os.RemoveAll(tmpHome) fn := filepath.Join(tmpHome, ConfigFileName) - if err := ioutil.WriteFile(fn, []byte("{}"), 0600); err != nil { - t.Fatal(err) - } + err = ioutil.WriteFile(fn, []byte("{}"), 0600) + require.NoError(t, err) config, err := Load(tmpHome) - if err != nil { - t.Fatalf("Failed loading on empty json file: %q", err) - } + require.NoError(t, err) // Now save it and make sure it shows up in new form saveConfigAndValidateNewFormat(t, config, tmpHome) @@ -118,9 +107,7 @@ email`: "Invalid auth configuration file", } tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer os.RemoveAll(tmpHome) homeKey := homedir.Key() @@ -131,27 +118,17 @@ email`: "Invalid auth configuration file", for content, expectedError := range invalids { fn := filepath.Join(tmpHome, oldConfigfile) - if err := ioutil.WriteFile(fn, []byte(content), 0600); err != nil { - t.Fatal(err) - } - - config, err := Load(tmpHome) - // Use Contains instead of == since the file name will change each time - if err == nil || !strings.Contains(err.Error(), expectedError) { - t.Fatalf("Should have failed\nConfig: %v\nGot: %v\nExpected: %v", config, err, expectedError) - } + err := ioutil.WriteFile(fn, []byte(content), 0600) + require.NoError(t, err) + _, err = Load(tmpHome) + testutil.ErrorContains(t, err, expectedError) } } func TestOldValidAuth(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatal(err) - } - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer os.RemoveAll(tmpHome) homeKey := homedir.Key() @@ -163,14 +140,11 @@ func TestOldValidAuth(t *testing.T) { fn := filepath.Join(tmpHome, oldConfigfile) js := `username = am9lam9lOmhlbGxv email = user@example.com` - if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { - t.Fatal(err) - } + err = ioutil.WriteFile(fn, []byte(js), 0600) + require.NoError(t, err) config, err := Load(tmpHome) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // defaultIndexserver is https://index.docker.io/v1/ ac := config.AuthConfigs["https://index.docker.io/v1/"] @@ -189,16 +163,12 @@ func TestOldValidAuth(t *testing.T) { } }` - if configStr != expConfStr { - t.Fatalf("Should have save in new form: \n%s\n not \n%s", configStr, expConfStr) - } + assert.Equal(t, expConfStr, configStr) } func TestOldJSONInvalid(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer os.RemoveAll(tmpHome) homeKey := homedir.Key() @@ -222,9 +192,7 @@ func TestOldJSONInvalid(t *testing.T) { func TestOldJSON(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer os.RemoveAll(tmpHome) homeKey := homedir.Key() @@ -240,9 +208,7 @@ func TestOldJSON(t *testing.T) { } config, err := Load(tmpHome) - if err != nil { - t.Fatalf("Failed loading on empty json file: %q", err) - } + require.NoError(t, err) ac := config.AuthConfigs["https://index.docker.io/v1/"] if ac.Username != "joejoe" || ac.Password != "hello" { @@ -268,9 +234,7 @@ func TestOldJSON(t *testing.T) { func TestNewJSON(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer os.RemoveAll(tmpHome) fn := filepath.Join(tmpHome, ConfigFileName) @@ -280,9 +244,7 @@ func TestNewJSON(t *testing.T) { } config, err := Load(tmpHome) - if err != nil { - t.Fatalf("Failed loading on empty json file: %q", err) - } + require.NoError(t, err) ac := config.AuthConfigs["https://index.docker.io/v1/"] if ac.Username != "joejoe" || ac.Password != "hello" { @@ -307,9 +269,7 @@ func TestNewJSON(t *testing.T) { func TestNewJSONNoEmail(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer os.RemoveAll(tmpHome) fn := filepath.Join(tmpHome, ConfigFileName) @@ -319,9 +279,7 @@ func TestNewJSONNoEmail(t *testing.T) { } config, err := Load(tmpHome) - if err != nil { - t.Fatalf("Failed loading on empty json file: %q", err) - } + require.NoError(t, err) ac := config.AuthConfigs["https://index.docker.io/v1/"] if ac.Username != "joejoe" || ac.Password != "hello" { @@ -346,9 +304,7 @@ func TestNewJSONNoEmail(t *testing.T) { func TestJSONWithPsFormat(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer os.RemoveAll(tmpHome) fn := filepath.Join(tmpHome, ConfigFileName) @@ -361,9 +317,7 @@ func TestJSONWithPsFormat(t *testing.T) { } config, err := Load(tmpHome) - if err != nil { - t.Fatalf("Failed loading on empty json file: %q", err) - } + require.NoError(t, err) if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` { t.Fatalf("Unknown ps format: %s\n", config.PsFormat) @@ -379,9 +333,7 @@ func TestJSONWithPsFormat(t *testing.T) { func TestJSONWithCredentialStore(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer os.RemoveAll(tmpHome) fn := filepath.Join(tmpHome, ConfigFileName) @@ -394,9 +346,7 @@ func TestJSONWithCredentialStore(t *testing.T) { } config, err := Load(tmpHome) - if err != nil { - t.Fatalf("Failed loading on empty json file: %q", err) - } + require.NoError(t, err) if config.CredentialsStore != "crazy-secure-storage" { t.Fatalf("Unknown credential store: %s\n", config.CredentialsStore) @@ -412,9 +362,7 @@ func TestJSONWithCredentialStore(t *testing.T) { func TestJSONWithCredentialHelpers(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer os.RemoveAll(tmpHome) fn := filepath.Join(tmpHome, ConfigFileName) @@ -427,9 +375,7 @@ func TestJSONWithCredentialHelpers(t *testing.T) { } config, err := Load(tmpHome) - if err != nil { - t.Fatalf("Failed loading on empty json file: %q", err) - } + require.NoError(t, err) if config.CredentialHelpers == nil { t.Fatal("config.CredentialHelpers was nil") @@ -450,26 +396,18 @@ func TestJSONWithCredentialHelpers(t *testing.T) { } // Save it and make sure it shows up in new form -func saveConfigAndValidateNewFormat(t *testing.T, config *configfile.ConfigFile, homeFolder string) string { - if err := config.Save(); err != nil { - t.Fatalf("Failed to save: %q", err) - } +func saveConfigAndValidateNewFormat(t *testing.T, config *configfile.ConfigFile, configDir string) string { + require.NoError(t, config.Save()) - buf, err := ioutil.ReadFile(filepath.Join(homeFolder, ConfigFileName)) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(string(buf), `"auths":`) { - t.Fatalf("Should have save in new form: %s", string(buf)) - } + buf, err := ioutil.ReadFile(filepath.Join(configDir, ConfigFileName)) + require.NoError(t, err) + assert.Contains(t, string(buf), `"auths":`) return string(buf) } func TestConfigDir(t *testing.T) { tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer os.RemoveAll(tmpHome) if Dir() == tmpHome { @@ -484,22 +422,11 @@ func TestConfigDir(t *testing.T) { } } -func TestConfigFile(t *testing.T) { - configFilename := "configFilename" - configFile := NewConfigFile(configFilename) - - if configFile.Filename != configFilename { - t.Fatalf("Expected %s, got %s", configFilename, configFile.Filename) - } -} - func TestJSONReaderNoFile(t *testing.T) { js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } } }` config, err := LoadFromReader(strings.NewReader(js)) - if err != nil { - t.Fatalf("Failed loading on empty json file: %q", err) - } + require.NoError(t, err) ac := config.AuthConfigs["https://index.docker.io/v1/"] if ac.Username != "joejoe" || ac.Password != "hello" { @@ -512,9 +439,7 @@ func TestOldJSONReaderNoFile(t *testing.T) { js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}` config, err := LegacyLoadFromReader(strings.NewReader(js)) - if err != nil { - t.Fatalf("Failed loading on empty json file: %q", err) - } + require.NoError(t, err) ac := config.AuthConfigs["https://index.docker.io/v1/"] if ac.Username != "joejoe" || ac.Password != "hello" { @@ -528,9 +453,7 @@ func TestJSONWithPsFormatNoFile(t *testing.T) { "psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}" }` config, err := LoadFromReader(strings.NewReader(js)) - if err != nil { - t.Fatalf("Failed loading on empty json file: %q", err) - } + require.NoError(t, err) if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` { t.Fatalf("Unknown ps format: %s\n", config.PsFormat) @@ -546,28 +469,19 @@ func TestJSONSaveWithNoFile(t *testing.T) { config, err := LoadFromReader(strings.NewReader(js)) require.NoError(t, err) err = config.Save() - if err == nil { - t.Fatalf("Expected error. File should not have been able to save with no file name.") - } + testutil.ErrorContains(t, err, "with empty filename") tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatalf("Failed to create a temp dir: %q", err) - } + require.NoError(t, err) defer os.RemoveAll(tmpHome) fn := filepath.Join(tmpHome, ConfigFileName) f, _ := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) defer f.Close() - err = config.SaveToWriter(f) - if err != nil { - t.Fatalf("Failed saving to file: %q", err) - } + require.NoError(t, config.SaveToWriter(f)) buf, err := ioutil.ReadFile(filepath.Join(tmpHome, ConfigFileName)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) expConfStr := `{ "auths": { "https://index.docker.io/v1/": { @@ -582,32 +496,23 @@ func TestJSONSaveWithNoFile(t *testing.T) { } func TestLegacyJSONSaveWithNoFile(t *testing.T) { - js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}` config, err := LegacyLoadFromReader(strings.NewReader(js)) require.NoError(t, err) err = config.Save() - if err == nil { - t.Fatalf("Expected error. File should not have been able to save with no file name.") - } + testutil.ErrorContains(t, err, "with empty filename") tmpHome, err := ioutil.TempDir("", "config-test") - if err != nil { - t.Fatalf("Failed to create a temp dir: %q", err) - } + require.NoError(t, err) defer os.RemoveAll(tmpHome) fn := filepath.Join(tmpHome, ConfigFileName) f, _ := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) defer f.Close() - if err = config.SaveToWriter(f); err != nil { - t.Fatalf("Failed saving to file: %q", err) - } + require.NoError(t, config.SaveToWriter(f)) buf, err := ioutil.ReadFile(filepath.Join(tmpHome, ConfigFileName)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) expConfStr := `{ "auths": { @@ -622,3 +527,22 @@ func TestLegacyJSONSaveWithNoFile(t *testing.T) { t.Fatalf("Should have save in new form: \n%s\n not \n%s", string(buf), expConfStr) } } + +func TestLoadDefaultConfigFile(t *testing.T) { + dir, cleanup := setupConfigDir(t) + defer cleanup() + buffer := new(bytes.Buffer) + + filename := filepath.Join(dir, ConfigFileName) + content := []byte(`{"PsFormat": "format"}`) + err := ioutil.WriteFile(filename, content, 0644) + require.NoError(t, err) + + configFile := LoadDefaultConfigFile(buffer) + credStore := credentials.DetectDefaultStore("") + expected := configfile.New(filename) + expected.CredentialsStore = credStore + expected.PsFormat = "format" + + assert.Equal(t, expected, configFile) +} diff --git a/components/cli/cli/config/configfile/file.go b/components/cli/cli/config/configfile/file.go index 9c2c4eec6d..babd63693b 100644 --- a/components/cli/cli/config/configfile/file.go +++ b/components/cli/cli/config/configfile/file.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" + "github.com/docker/cli/cli/config/credentials" "github.com/docker/cli/opts" "github.com/docker/docker/api/types" "github.com/pkg/errors" @@ -53,6 +54,15 @@ type ProxyConfig struct { FTPProxy string `json:"ftpProxy,omitempty"` } +// New initializes an empty configuration file for the given filename 'fn' +func New(fn string) *ConfigFile { + return &ConfigFile{ + AuthConfigs: make(map[string]types.AuthConfig), + HTTPHeaders: make(map[string]string), + Filename: fn, + } +} + // LegacyLoadFromReader reads the non-nested configuration data given and sets up the // auth config information with given directory and populates the receiver object func (configFile *ConfigFile) LegacyLoadFromReader(configData io.Reader) error { @@ -118,6 +128,11 @@ func (configFile *ConfigFile) ContainsAuth() bool { len(configFile.AuthConfigs) > 0 } +// GetAuthConfigs returns the mapping of repo to auth configuration +func (configFile *ConfigFile) GetAuthConfigs() map[string]types.AuthConfig { + return configFile.AuthConfigs +} + // SaveToWriter encodes and writes out all the authorization information to // the given writer func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error { @@ -231,3 +246,56 @@ func decodeAuth(authStr string) (string, string, error) { password := strings.Trim(arr[1], "\x00") return arr[0], password, nil } + +// GetCredentialsStore returns a new credentials store from the settings in the +// configuration file +func (configFile *ConfigFile) GetCredentialsStore(serverAddress string) credentials.Store { + if helper := getConfiguredCredentialStore(configFile, serverAddress); helper != "" { + return credentials.NewNativeStore(configFile, helper) + } + return credentials.NewFileStore(configFile) +} + +// GetAuthConfig for a repository from the credential store +func (configFile *ConfigFile) GetAuthConfig(serverAddress string) (types.AuthConfig, error) { + return configFile.GetCredentialsStore(serverAddress).Get(serverAddress) +} + +// getConfiguredCredentialStore returns the credential helper configured for the +// given registry, the default credsStore, or the empty string if neither are +// configured. +func getConfiguredCredentialStore(c *ConfigFile, serverAddress string) string { + if c.CredentialHelpers != nil && serverAddress != "" { + if helper, exists := c.CredentialHelpers[serverAddress]; exists { + return helper + } + } + return c.CredentialsStore +} + +// GetAllCredentials returns all of the credentials stored in all of the +// configured credential stores. +func (configFile *ConfigFile) GetAllCredentials() (map[string]types.AuthConfig, error) { + auths := make(map[string]types.AuthConfig) + addAll := func(from map[string]types.AuthConfig) { + for reg, ac := range from { + auths[reg] = ac + } + } + + for registry := range configFile.CredentialHelpers { + helper := configFile.GetCredentialsStore(registry) + newAuths, err := helper.GetAll() + if err != nil { + return nil, err + } + addAll(newAuths) + } + defaultStore := configFile.GetCredentialsStore("") + newAuths, err := defaultStore.GetAll() + if err != nil { + return nil, err + } + addAll(newAuths) + return auths, nil +} diff --git a/components/cli/cli/config/configfile/file_test.go b/components/cli/cli/config/configfile/file_test.go index 8c84347719..f2a61b1794 100644 --- a/components/cli/cli/config/configfile/file_test.go +++ b/components/cli/cli/config/configfile/file_test.go @@ -6,26 +6,18 @@ import ( "github.com/docker/docker/api/types" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestEncodeAuth(t *testing.T) { newAuthConfig := &types.AuthConfig{Username: "ken", Password: "test"} authStr := encodeAuth(newAuthConfig) - decAuthConfig := &types.AuthConfig{} + + expected := &types.AuthConfig{} var err error - decAuthConfig.Username, decAuthConfig.Password, err = decodeAuth(authStr) - if err != nil { - t.Fatal(err) - } - if newAuthConfig.Username != decAuthConfig.Username { - t.Fatal("Encode Username doesn't match decoded Username") - } - if newAuthConfig.Password != decAuthConfig.Password { - t.Fatal("Encode Password doesn't match decoded Password") - } - if authStr != "a2VuOnRlc3Q=" { - t.Fatal("AuthString encoding isn't correct.") - } + expected.Username, expected.Password, err = decodeAuth(authStr) + require.NoError(t, err) + assert.Equal(t, expected, newAuthConfig) } func TestProxyConfig(t *testing.T) { @@ -142,3 +134,26 @@ func TestProxyConfigPerHost(t *testing.T) { } assert.Equal(t, expected, proxyConfig) } + +func TestConfigFile(t *testing.T) { + configFilename := "configFilename" + configFile := New(configFilename) + + assert.Equal(t, configFilename, configFile.Filename) +} + +func TestGetAllCredentials(t *testing.T) { + configFile := New("filename") + exampleAuth := types.AuthConfig{ + Username: "user", + Password: "pass", + } + configFile.AuthConfigs["example.com/foo"] = exampleAuth + + authConfigs, err := configFile.GetAllCredentials() + require.NoError(t, err) + + expected := make(map[string]types.AuthConfig) + expected["example.com/foo"] = exampleAuth + assert.Equal(t, expected, authConfigs) +} diff --git a/components/cli/cli/config/credentials/default_store.go b/components/cli/cli/config/credentials/default_store.go index dc080dbd40..b2cc4df8bc 100644 --- a/components/cli/cli/config/credentials/default_store.go +++ b/components/cli/cli/config/credentials/default_store.go @@ -2,21 +2,18 @@ package credentials import ( "os/exec" - - "github.com/docker/cli/cli/config/configfile" ) -// DetectDefaultStore sets the default credentials store -// if the host includes the default store helper program. -func DetectDefaultStore(c *configfile.ConfigFile) { - if c.CredentialsStore != "" { - // user defined - return +// DetectDefaultStore return the default credentials store for the platform if +// the store executable is available. +func DetectDefaultStore(store string) string { + // user defined or no default for platform + if store != "" || defaultCredentialsStore == "" { + return store } - if defaultCredentialsStore != "" { - if _, err := exec.LookPath(remoteCredentialsPrefix + defaultCredentialsStore); err == nil { - c.CredentialsStore = defaultCredentialsStore - } + if _, err := exec.LookPath(remoteCredentialsPrefix + defaultCredentialsStore); err == nil { + return defaultCredentialsStore } + return "" } diff --git a/components/cli/cli/config/credentials/file_store.go b/components/cli/cli/config/credentials/file_store.go index 4e3325c79b..b186dbbed6 100644 --- a/components/cli/cli/config/credentials/file_store.go +++ b/components/cli/cli/config/credentials/file_store.go @@ -1,37 +1,39 @@ package credentials import ( - "github.com/docker/cli/cli/config/configfile" "github.com/docker/docker/api/types" "github.com/docker/docker/registry" ) +type store interface { + Save() error + GetAuthConfigs() map[string]types.AuthConfig +} + // fileStore implements a credentials store using // the docker configuration file to keep the credentials in plain text. type fileStore struct { - file *configfile.ConfigFile + file store } // NewFileStore creates a new file credentials store. -func NewFileStore(file *configfile.ConfigFile) Store { - return &fileStore{ - file: file, - } +func NewFileStore(file store) Store { + return &fileStore{file: file} } // Erase removes the given credentials from the file store. func (c *fileStore) Erase(serverAddress string) error { - delete(c.file.AuthConfigs, serverAddress) + delete(c.file.GetAuthConfigs(), serverAddress) return c.file.Save() } // Get retrieves credentials for a specific server from the file store. func (c *fileStore) Get(serverAddress string) (types.AuthConfig, error) { - authConfig, ok := c.file.AuthConfigs[serverAddress] + authConfig, ok := c.file.GetAuthConfigs()[serverAddress] if !ok { // Maybe they have a legacy config file, we will iterate the keys converting // them to the new format and testing - for r, ac := range c.file.AuthConfigs { + for r, ac := range c.file.GetAuthConfigs() { if serverAddress == registry.ConvertToHostname(r) { return ac, nil } @@ -43,11 +45,11 @@ func (c *fileStore) Get(serverAddress string) (types.AuthConfig, error) { } func (c *fileStore) GetAll() (map[string]types.AuthConfig, error) { - return c.file.AuthConfigs, nil + return c.file.GetAuthConfigs(), nil } // Store saves the given credentials in the file store. func (c *fileStore) Store(authConfig types.AuthConfig) error { - c.file.AuthConfigs[authConfig.ServerAddress] = authConfig + c.file.GetAuthConfigs()[authConfig.ServerAddress] = authConfig return c.file.Save() } diff --git a/components/cli/cli/config/credentials/file_store_test.go b/components/cli/cli/config/credentials/file_store_test.go index 6e16f2cd32..0d4fbd6bdc 100644 --- a/components/cli/cli/config/credentials/file_store_test.go +++ b/components/cli/cli/config/credentials/file_store_test.go @@ -1,56 +1,49 @@ package credentials import ( - "io/ioutil" "testing" - cliconfig "github.com/docker/cli/cli/config" - "github.com/docker/cli/cli/config/configfile" "github.com/docker/docker/api/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func newConfigFile(auths map[string]types.AuthConfig) *configfile.ConfigFile { - tmp, _ := ioutil.TempFile("", "docker-test") - name := tmp.Name() - tmp.Close() +type fakeStore struct { + configs map[string]types.AuthConfig +} - c := cliconfig.NewConfigFile(name) - c.AuthConfigs = auths - return c +func (f *fakeStore) Save() error { + return nil +} + +func (f *fakeStore) GetAuthConfigs() map[string]types.AuthConfig { + return f.configs +} + +func newStore(auths map[string]types.AuthConfig) store { + return &fakeStore{configs: auths} } func TestFileStoreAddCredentials(t *testing.T) { - f := newConfigFile(make(map[string]types.AuthConfig)) + f := newStore(make(map[string]types.AuthConfig)) s := NewFileStore(f) - err := s.Store(types.AuthConfig{ + auth := types.AuthConfig{ Auth: "super_secret_token", Email: "foo@example.com", ServerAddress: "https://example.com", - }) + } + err := s.Store(auth) + require.NoError(t, err) + assert.Len(t, f.GetAuthConfigs(), 1) - if err != nil { - t.Fatal(err) - } - - if len(f.AuthConfigs) != 1 { - t.Fatalf("expected 1 auth config, got %d", len(f.AuthConfigs)) - } - - a, ok := f.AuthConfigs["https://example.com"] - if !ok { - t.Fatalf("expected auth for https://example.com, got %v", f.AuthConfigs) - } - if a.Auth != "super_secret_token" { - t.Fatalf("expected auth `super_secret_token`, got %s", a.Auth) - } - if a.Email != "foo@example.com" { - t.Fatalf("expected email `foo@example.com`, got %s", a.Email) - } + actual, ok := f.GetAuthConfigs()["https://example.com"] + assert.True(t, ok) + assert.Equal(t, auth, actual) } func TestFileStoreGet(t *testing.T) { - f := newConfigFile(map[string]types.AuthConfig{ + f := newStore(map[string]types.AuthConfig{ "https://example.com": { Auth: "super_secret_token", Email: "foo@example.com", @@ -74,7 +67,7 @@ func TestFileStoreGet(t *testing.T) { func TestFileStoreGetAll(t *testing.T) { s1 := "https://example.com" s2 := "https://example2.com" - f := newConfigFile(map[string]types.AuthConfig{ + f := newStore(map[string]types.AuthConfig{ s1: { Auth: "super_secret_token", Email: "foo@example.com", @@ -110,7 +103,7 @@ func TestFileStoreGetAll(t *testing.T) { } func TestFileStoreErase(t *testing.T) { - f := newConfigFile(map[string]types.AuthConfig{ + f := newStore(map[string]types.AuthConfig{ "https://example.com": { Auth: "super_secret_token", Email: "foo@example.com", diff --git a/components/cli/cli/config/credentials/native_store.go b/components/cli/cli/config/credentials/native_store.go index cef34db92f..ef3aab4ad3 100644 --- a/components/cli/cli/config/credentials/native_store.go +++ b/components/cli/cli/config/credentials/native_store.go @@ -1,7 +1,6 @@ package credentials import ( - "github.com/docker/cli/cli/config/configfile" "github.com/docker/docker-credential-helpers/client" "github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker/api/types" @@ -22,7 +21,7 @@ type nativeStore struct { // NewNativeStore creates a new native store that // uses a remote helper program to manage credentials. -func NewNativeStore(file *configfile.ConfigFile, helperSuffix string) Store { +func NewNativeStore(file store, helperSuffix string) Store { name := remoteCredentialsPrefix + helperSuffix return &nativeStore{ programFunc: client.NewShellProgramFunc(name), diff --git a/components/cli/cli/config/credentials/native_store_test.go b/components/cli/cli/config/credentials/native_store_test.go index 360cc20efc..582c2bd598 100644 --- a/components/cli/cli/config/credentials/native_store_test.go +++ b/components/cli/cli/config/credentials/native_store_test.go @@ -11,7 +11,10 @@ import ( "github.com/docker/docker-credential-helpers/client" "github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/testutil" "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( @@ -90,53 +93,32 @@ func mockCommandFn(args ...string) client.Program { } func TestNativeStoreAddCredentials(t *testing.T) { - f := newConfigFile(make(map[string]types.AuthConfig)) - f.CredentialsStore = "mock" - + f := newStore(make(map[string]types.AuthConfig)) s := &nativeStore{ programFunc: mockCommandFn, fileStore: NewFileStore(f), } - err := s.Store(types.AuthConfig{ + auth := types.AuthConfig{ Username: "foo", Password: "bar", Email: "foo@example.com", ServerAddress: validServerAddress, - }) + } + err := s.Store(auth) + require.NoError(t, err) + assert.Len(t, f.GetAuthConfigs(), 1) - if err != nil { - t.Fatal(err) - } - - if len(f.AuthConfigs) != 1 { - t.Fatalf("expected 1 auth config, got %d", len(f.AuthConfigs)) - } - - a, ok := f.AuthConfigs[validServerAddress] - if !ok { - t.Fatalf("expected auth for %s, got %v", validServerAddress, f.AuthConfigs) - } - if a.Auth != "" { - t.Fatalf("expected auth to be empty, got %s", a.Auth) - } - if a.Username != "" { - t.Fatalf("expected username to be empty, got %s", a.Username) - } - if a.Password != "" { - t.Fatalf("expected password to be empty, got %s", a.Password) - } - if a.IdentityToken != "" { - t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken) - } - if a.Email != "foo@example.com" { - t.Fatalf("expected email `foo@example.com`, got %s", a.Email) + actual, ok := f.GetAuthConfigs()[validServerAddress] + assert.True(t, ok) + expected := types.AuthConfig{ + Email: auth.Email, + ServerAddress: auth.ServerAddress, } + assert.Equal(t, expected, actual) } func TestNativeStoreAddInvalidCredentials(t *testing.T) { - f := newConfigFile(make(map[string]types.AuthConfig)) - f.CredentialsStore = "mock" - + f := newStore(make(map[string]types.AuthConfig)) s := &nativeStore{ programFunc: mockCommandFn, fileStore: NewFileStore(f), @@ -147,102 +129,66 @@ func TestNativeStoreAddInvalidCredentials(t *testing.T) { Email: "foo@example.com", ServerAddress: invalidServerAddress, }) - - if err == nil { - t.Fatal("expected error, got nil") - } - - if !strings.Contains(err.Error(), "program failed") { - t.Fatalf("expected `program failed`, got %v", err) - } - - if len(f.AuthConfigs) != 0 { - t.Fatalf("expected 0 auth config, got %d", len(f.AuthConfigs)) - } + testutil.ErrorContains(t, err, "program failed") + assert.Len(t, f.GetAuthConfigs(), 0) } func TestNativeStoreGet(t *testing.T) { - f := newConfigFile(map[string]types.AuthConfig{ + f := newStore(map[string]types.AuthConfig{ validServerAddress: { Email: "foo@example.com", }, }) - f.CredentialsStore = "mock" - s := &nativeStore{ programFunc: mockCommandFn, fileStore: NewFileStore(f), } - a, err := s.Get(validServerAddress) - if err != nil { - t.Fatal(err) - } + actual, err := s.Get(validServerAddress) + require.NoError(t, err) - if a.Username != "foo" { - t.Fatalf("expected username `foo`, got %s", a.Username) - } - if a.Password != "bar" { - t.Fatalf("expected password `bar`, got %s", a.Password) - } - if a.IdentityToken != "" { - t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken) - } - if a.Email != "foo@example.com" { - t.Fatalf("expected email `foo@example.com`, got %s", a.Email) + expected := types.AuthConfig{ + Username: "foo", + Password: "bar", + Email: "foo@example.com", } + assert.Equal(t, expected, actual) } func TestNativeStoreGetIdentityToken(t *testing.T) { - f := newConfigFile(map[string]types.AuthConfig{ + f := newStore(map[string]types.AuthConfig{ validServerAddress2: { Email: "foo@example2.com", }, }) - f.CredentialsStore = "mock" s := &nativeStore{ programFunc: mockCommandFn, fileStore: NewFileStore(f), } - a, err := s.Get(validServerAddress2) - if err != nil { - t.Fatal(err) - } + actual, err := s.Get(validServerAddress2) + require.NoError(t, err) - if a.Username != "" { - t.Fatalf("expected username to be empty, got %s", a.Username) - } - if a.Password != "" { - t.Fatalf("expected password to be empty, got %s", a.Password) - } - if a.IdentityToken != "abcd1234" { - t.Fatalf("expected identity token `abcd1234`, got %s", a.IdentityToken) - } - if a.Email != "foo@example2.com" { - t.Fatalf("expected email `foo@example2.com`, got %s", a.Email) + expected := types.AuthConfig{ + IdentityToken: "abcd1234", + Email: "foo@example2.com", } + assert.Equal(t, expected, actual) } func TestNativeStoreGetAll(t *testing.T) { - f := newConfigFile(map[string]types.AuthConfig{ + f := newStore(map[string]types.AuthConfig{ validServerAddress: { Email: "foo@example.com", }, }) - f.CredentialsStore = "mock" s := &nativeStore{ programFunc: mockCommandFn, fileStore: NewFileStore(f), } as, err := s.GetAll() - if err != nil { - t.Fatal(err) - } - - if len(as) != 2 { - t.Fatalf("wanted 2, got %d", len(as)) - } + require.NoError(t, err) + assert.Len(t, as, 2) if as[validServerAddress].Username != "foo" { t.Fatalf("expected username `foo` for %s, got %s", validServerAddress, as[validServerAddress].Username) @@ -271,86 +217,62 @@ func TestNativeStoreGetAll(t *testing.T) { } func TestNativeStoreGetMissingCredentials(t *testing.T) { - f := newConfigFile(map[string]types.AuthConfig{ + f := newStore(map[string]types.AuthConfig{ validServerAddress: { Email: "foo@example.com", }, }) - f.CredentialsStore = "mock" s := &nativeStore{ programFunc: mockCommandFn, fileStore: NewFileStore(f), } _, err := s.Get(missingCredsAddress) - if err != nil { - // missing credentials do not produce an error - t.Fatal(err) - } + assert.NoError(t, err) } func TestNativeStoreGetInvalidAddress(t *testing.T) { - f := newConfigFile(map[string]types.AuthConfig{ + f := newStore(map[string]types.AuthConfig{ validServerAddress: { Email: "foo@example.com", }, }) - f.CredentialsStore = "mock" s := &nativeStore{ programFunc: mockCommandFn, fileStore: NewFileStore(f), } _, err := s.Get(invalidServerAddress) - if err == nil { - t.Fatal("expected error, got nil") - } - - if !strings.Contains(err.Error(), "program failed") { - t.Fatalf("expected `program failed`, got %v", err) - } + testutil.ErrorContains(t, err, "program failed") } func TestNativeStoreErase(t *testing.T) { - f := newConfigFile(map[string]types.AuthConfig{ + f := newStore(map[string]types.AuthConfig{ validServerAddress: { Email: "foo@example.com", }, }) - f.CredentialsStore = "mock" s := &nativeStore{ programFunc: mockCommandFn, fileStore: NewFileStore(f), } err := s.Erase(validServerAddress) - if err != nil { - t.Fatal(err) - } - - if len(f.AuthConfigs) != 0 { - t.Fatalf("expected 0 auth configs, got %d", len(f.AuthConfigs)) - } + require.NoError(t, err) + assert.Len(t, f.GetAuthConfigs(), 0) } func TestNativeStoreEraseInvalidAddress(t *testing.T) { - f := newConfigFile(map[string]types.AuthConfig{ + f := newStore(map[string]types.AuthConfig{ validServerAddress: { Email: "foo@example.com", }, }) - f.CredentialsStore = "mock" s := &nativeStore{ programFunc: mockCommandFn, fileStore: NewFileStore(f), } err := s.Erase(invalidServerAddress) - if err == nil { - t.Fatal("expected error, got nil") - } - - if !strings.Contains(err.Error(), "program failed") { - t.Fatalf("expected `program failed`, got %v", err) - } + testutil.ErrorContains(t, err, "program failed") } diff --git a/components/cli/cli/internal/test/builders/task.go b/components/cli/cli/internal/test/builders/task.go index b4551c983b..479b6f14c5 100644 --- a/components/cli/cli/internal/test/builders/task.go +++ b/components/cli/cli/internal/test/builders/task.go @@ -70,6 +70,13 @@ func TaskDesiredState(state swarm.TaskState) func(*swarm.Task) { } } +// TaskSlot sets the task's slot +func TaskSlot(slot int) func(*swarm.Task) { + return func(task *swarm.Task) { + task.Slot = slot + } +} + // WithStatus sets the task status func WithStatus(statusBuilders ...func(*swarm.TaskStatus)) func(*swarm.Task) { return func(task *swarm.Task) { diff --git a/components/cli/cli/internal/test/cli.go b/components/cli/cli/internal/test/cli.go index f0f75f7bd5..378ee9cdc2 100644 --- a/components/cli/cli/internal/test/cli.go +++ b/components/cli/cli/internal/test/cli.go @@ -7,7 +7,6 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config/configfile" - "github.com/docker/cli/cli/config/credentials" "github.com/docker/docker/client" ) @@ -19,16 +18,17 @@ type FakeCli struct { out *command.OutStream err io.Writer in *command.InStream - store credentials.Store + server command.ServerInfo } // NewFakeCli returns a Cli backed by the fakeCli func NewFakeCli(client client.APIClient, out io.Writer) *FakeCli { return &FakeCli{ - client: client, - out: command.NewOutStream(out), - err: ioutil.Discard, - in: command.NewInStream(ioutil.NopCloser(strings.NewReader(""))), + client: client, + out: command.NewOutStream(out), + err: ioutil.Discard, + in: command.NewInStream(ioutil.NopCloser(strings.NewReader(""))), + configfile: configfile.New("configfile"), } } @@ -72,10 +72,7 @@ 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 +// ServerInfo returns API server information for the server used by this client +func (c *FakeCli) ServerInfo() command.ServerInfo { + return c.server } diff --git a/components/cli/cmd/docker/daemon_unit_test.go b/components/cli/cmd/docker/daemon_unit_test.go index ffd8a5e2f5..b7bb441136 100644 --- a/components/cli/cmd/docker/daemon_unit_test.go +++ b/components/cli/cmd/docker/daemon_unit_test.go @@ -3,6 +3,7 @@ package main import ( + "io/ioutil" "testing" "github.com/spf13/cobra" @@ -17,6 +18,7 @@ func TestDaemonCommandHelp(t *testing.T) { cmd := newDaemonCommand() cmd.RunE = stubRun cmd.SetArgs([]string{"--help"}) + cmd.SetOutput(ioutil.Discard) err := cmd.Execute() assert.NoError(t, err) } @@ -25,6 +27,7 @@ func TestDaemonCommand(t *testing.T) { cmd := newDaemonCommand() cmd.RunE = stubRun cmd.SetArgs([]string{"--containerd", "/foo"}) + cmd.SetOutput(ioutil.Discard) err := cmd.Execute() assert.NoError(t, err) } diff --git a/components/cli/contrib/completion/bash/docker b/components/cli/contrib/completion/bash/docker index 79209c2941..33ada275b1 100644 --- a/components/cli/contrib/completion/bash/docker +++ b/components/cli/contrib/completion/bash/docker @@ -2704,7 +2704,7 @@ _docker_network_connect() { _docker_network_create() { case "$prev" in - --aux-address|--gateway|--internal|--ip-range|--ipam-opt|--ipv6|--opt|-o|--subnet) + --aux-address|--gateway|--ip-range|--ipam-opt|--ipv6|--opt|-o|--subnet) return ;; --ipam-driver) @@ -2723,7 +2723,7 @@ _docker_network_create() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--attachable --aux-address --driver -d --gateway --help --internal --ip-range --ipam-driver --ipam-opt --ipv6 --label --opt -o --subnet" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--attachable --aux-address --driver -d --gateway --help --ingress --internal --ip-range --ipam-driver --ipam-opt --ipv6 --label --opt -o --subnet" -- "$cur" ) ) ;; esac } @@ -3039,6 +3039,7 @@ _docker_service_update() { _docker_service_update_and_create() { local options_with_args=" --endpoint-mode + --entrypoint --env -e --force --health-cmd @@ -3065,6 +3066,7 @@ _docker_service_update_and_create() { --rollback-failure-action --rollback-max-failure-ratio --rollback-monitor + --rollback-order --rollback-parallelism --stop-grace-period --stop-signal @@ -3072,12 +3074,14 @@ _docker_service_update_and_create() { --update-failure-action --update-max-failure-ratio --update-monitor + --update-order --update-parallelism --user -u --workdir -w " local boolean_options=" + --detach -d --help --no-healthcheck --read-only @@ -3138,7 +3142,7 @@ _docker_service_update_and_create() { fi if [ "$subcommand" = "update" ] ; then options_with_args="$options_with_args - --arg + --args --constraint-add --constraint-rm --container-label-add @@ -3154,6 +3158,8 @@ _docker_service_update_and_create() { --host-add --host-rm --image + --network-add + --network-rm --placement-pref-add --placement-pref-rm --publish-add @@ -3180,6 +3186,10 @@ _docker_service_update_and_create() { __docker_complete_image_repos_and_tags return ;; + --network-add|--network-rm) + __docker_complete_networks + return + ;; --placement-pref-add|--placement-pref-rm) COMPREPLY=( $( compgen -W "spread" -S = -- "$cur" ) ) __docker_nospace @@ -3240,6 +3250,10 @@ _docker_service_update_and_create() { COMPREPLY=( $( compgen -W "continue pause rollback" -- "$cur" ) ) return ;; + --update-order|--rollback-order) + COMPREPLY=( $( compgen -W "start-first stop-first" -- "$cur" ) ) + return + ;; --user|-u) __docker_complete_user_group return @@ -3270,6 +3284,7 @@ _docker_service_update_and_create() { _docker_swarm() { local subcommands=" + ca init join join-token @@ -3290,6 +3305,24 @@ _docker_swarm() { esac } +_docker_swarm_ca() { + case "$prev" in + --ca-cert|--ca-key) + _filedir + return + ;; + --cert-expiry|--external-ca) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--ca-cert --ca-key --cert-expiry --detach -d --external-ca --help --quiet -q --rotate" -- "$cur" ) ) + ;; + esac +} + _docker_swarm_init() { case "$prev" in --advertise-addr) @@ -3308,6 +3341,10 @@ _docker_swarm_init() { --cert-expiry|--dispatcher-heartbeat|--external-ca|--max-snapshots|--snapshot-interval|--task-history-limit) return ;; + --data-path-addr) + __docker_complete_local_interfaces + return + ;; --listen-addr) if [[ $cur == *: ]] ; then COMPREPLY=( $( compgen -W "2377" -- "${cur##*:}" ) ) @@ -3321,7 +3358,7 @@ _docker_swarm_init() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--advertise-addr --data-path-addr --autolock --availability --cert-expiry --dispatcher-heartbeat --external-ca --force-new-cluster --help --listen-addr --max-snapshots --snapshot-interval --task-history-limit" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--advertise-addr --autolock --availability --cert-expiry --data-path-addr --dispatcher-heartbeat --external-ca --force-new-cluster --help --listen-addr --max-snapshots --snapshot-interval --task-history-limit" -- "$cur" ) ) ;; esac } @@ -3337,6 +3374,14 @@ _docker_swarm_join() { fi return ;; + --availability) + COMPREPLY=( $( compgen -W "active drain pause" -- "$cur" ) ) + return + ;; + --data-path-addr) + __docker_complete_local_interfaces + return + ;; --listen-addr) if [[ $cur == *: ]] ; then COMPREPLY=( $( compgen -W "2377" -- "${cur##*:}" ) ) @@ -3346,10 +3391,6 @@ _docker_swarm_join() { fi return ;; - --availability) - COMPREPLY=( $( compgen -W "active drain pause" -- "$cur" ) ) - return - ;; --token) return ;; @@ -3357,7 +3398,7 @@ _docker_swarm_join() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--advertise-addr --data-path-addr --availability --help --listen-addr --token" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--advertise-addr --availability --data-path-addr --help --listen-addr --token" -- "$cur" ) ) ;; *:) COMPREPLY=( $( compgen -W "2377" -- "${cur##*:}" ) ) @@ -4230,13 +4271,16 @@ _docker_system_events() { destroy detach die + disable disconnect + enable exec_create exec_detach exec_start export health_status import + install kill load mount @@ -4245,6 +4289,7 @@ _docker_system_events() { pull push reload + remove rename resize restart @@ -4270,7 +4315,7 @@ _docker_system_events() { return ;; type) - COMPREPLY=( $( compgen -W "container daemon image network volume" -- "${cur##*=}" ) ) + COMPREPLY=( $( compgen -W "container daemon image network plugin volume" -- "${cur##*=}" ) ) return ;; volume) @@ -4322,7 +4367,7 @@ _docker_system_prune() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--all -a --force -f --filter --help" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--all -a --force -f --filter --help --volumes" -- "$cur" ) ) ;; esac } diff --git a/components/cli/docs/reference/builder.md b/components/cli/docs/reference/builder.md index 9ab4ba4df9..d6274407e6 100644 --- a/components/cli/docs/reference/builder.md +++ b/components/cli/docs/reference/builder.md @@ -30,13 +30,13 @@ Practices](https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-pr ## Usage The [`docker build`](commandline/build.md) command builds an image from -a `Dockerfile` and a *context*. The build's context is the files at a specified -location `PATH` or `URL`. The `PATH` is a directory on your local filesystem. -The `URL` is a Git repository location. +a `Dockerfile` and a *context*. The build's context is the set of files at a +specified location `PATH` or `URL`. The `PATH` is a directory on your local +filesystem. The `URL` is a Git repository location. A context is processed recursively. So, a `PATH` includes any subdirectories and -the `URL` includes the repository and its submodules. A simple build command -that uses the current directory as context: +the `URL` includes the repository and its submodules. This example shows a +build command that uses the current directory as context: $ docker build . Sending build context to Docker daemon 6.51 MB @@ -1328,9 +1328,9 @@ The `WORKDIR` instruction sets the working directory for any `RUN`, `CMD`, If the `WORKDIR` doesn't exist, it will be created even if it's not used in any subsequent `Dockerfile` instruction. -It can be used multiple times in the one `Dockerfile`. If a relative path -is provided, it will be relative to the path of the previous `WORKDIR` -instruction. For example: +The `WORKDIR` instruction can be used multiple times in a `Dockerfile`. If a +relative path is provided, it will be relative to the path of the previous +`WORKDIR` instruction. For example: WORKDIR /a WORKDIR b diff --git a/components/cli/docs/reference/commandline/build.md b/components/cli/docs/reference/commandline/build.md index 9f587372c6..443ea015b2 100644 --- a/components/cli/docs/reference/commandline/build.md +++ b/components/cli/docs/reference/commandline/build.md @@ -63,11 +63,11 @@ Options: ## Description -Builds Docker images from a Dockerfile and a "context". A build's context is -the files located in the specified `PATH` or `URL`. The build process can refer -to any of the files in the context. For example, your build can use an -[*ADD*](../builder.md#add) instruction to reference a file in the -context. +The `docker build` command builds Docker images from a Dockerfile and a +"context". A build's context is the set of files located in the specified +`PATH` or `URL`. The build process can refer to any of the files in the +context. For example, your build can use a [*COPY*](../builder.md#copy) +instruction to reference a file in the context. The `URL` parameter can refer to three kinds of resources: Git repositories, pre-packaged tarball contexts and plain text files. @@ -88,7 +88,7 @@ user credentials, VPN's, and so forth. Git URLs accept context configuration in their fragment section, separated by a colon `:`. The first part represents the reference that Git will check out, -this can be either a branch, a tag, or a remote reference. The second part +and can be either a branch, a tag, or a remote reference. The second part represents a subdirectory inside the repository that will be used as a build context. diff --git a/components/cli/docs/reference/commandline/dockerd.md b/components/cli/docs/reference/commandline/dockerd.md index 95a953f1c4..3e2d434974 100644 --- a/components/cli/docs/reference/commandline/dockerd.md +++ b/components/cli/docs/reference/commandline/dockerd.md @@ -740,6 +740,18 @@ to add multiple lower directory support for OverlayFS. This option should only be used after verifying this support exists in the kernel. Applying this option on a kernel without this support will cause failures on mount. +##### `overlay2.size` + +Sets the default max size of the container. It is supported only when the +backing fs is `xfs` and mounted with `pquota` mount option. Under these +conditions the user can pass any size less then the backing fs size. + +###### Example + +```bash +$ sudo dockerd -s overlay2 --storage-opt overlay2.size=1G +``` + ### Docker runtime execution options The Docker daemon relies on a diff --git a/components/cli/vendor.conf b/components/cli/vendor.conf index 5fff08c282..9e7742e658 100755 --- a/components/cli/vendor.conf +++ b/components/cli/vendor.conf @@ -10,7 +10,7 @@ github.com/docker/distribution b38e5838b7b2f2ad48e06ec4b500011976080621 github.com/docker/docker 050c1bb17bd033e909cb653f5449b683608293d6 github.com/docker/docker-credential-helpers v0.5.1 github.com/docker/go d30aec9fd63c35133f8f79c3412ad91a3b08be06 #? -github.com/docker/go-connections e15c02316c12de00874640cd76311849de2aeed5 +github.com/docker/go-connections 3ede32e2033de7505e6500d6c868c2b9ed9f169d github.com/docker/go-events 18b43f1bc85d9cdd42c05a6cd2d444c7a200a894 github.com/docker/go-units 9e638d38cf6977a37a8ea0078f3ee75a7cdb2dd1 github.com/docker/libtrust 9cbd2a1374f46905c68a4eb3694a130610adc62a diff --git a/components/cli/vendor/github.com/docker/go-connections/tlsconfig/certpool_go17.go b/components/cli/vendor/github.com/docker/go-connections/tlsconfig/certpool_go17.go index 1d5fa4c76d..1ca0965e06 100644 --- a/components/cli/vendor/github.com/docker/go-connections/tlsconfig/certpool_go17.go +++ b/components/cli/vendor/github.com/docker/go-connections/tlsconfig/certpool_go17.go @@ -5,8 +5,6 @@ package tlsconfig import ( "crypto/x509" "runtime" - - "github.com/Sirupsen/logrus" ) // SystemCertPool returns a copy of the system cert pool, @@ -14,7 +12,6 @@ import ( func SystemCertPool() (*x509.CertPool, error) { certpool, err := x509.SystemCertPool() if err != nil && runtime.GOOS == "windows" { - logrus.Infof("Unable to use system certificate pool: %v", err) return x509.NewCertPool(), nil } return certpool, err diff --git a/components/cli/vendor/github.com/docker/go-connections/tlsconfig/certpool_other.go b/components/cli/vendor/github.com/docker/go-connections/tlsconfig/certpool_other.go index 262c95e8cd..9ca974539a 100644 --- a/components/cli/vendor/github.com/docker/go-connections/tlsconfig/certpool_other.go +++ b/components/cli/vendor/github.com/docker/go-connections/tlsconfig/certpool_other.go @@ -5,12 +5,10 @@ package tlsconfig import ( "crypto/x509" - "github.com/Sirupsen/logrus" ) // SystemCertPool returns an new empty cert pool, // accessing system cert pool is supported in go 1.7 func SystemCertPool() (*x509.CertPool, error) { - logrus.Warn("Unable to use system certificate pool: requires building with go 1.7 or later") return x509.NewCertPool(), nil } diff --git a/components/cli/vendor/github.com/docker/go-connections/tlsconfig/config.go b/components/cli/vendor/github.com/docker/go-connections/tlsconfig/config.go index ad4b112ab3..1b31bbb8b1 100644 --- a/components/cli/vendor/github.com/docker/go-connections/tlsconfig/config.go +++ b/components/cli/vendor/github.com/docker/go-connections/tlsconfig/config.go @@ -13,7 +13,6 @@ import ( "io/ioutil" "os" - "github.com/Sirupsen/logrus" "github.com/pkg/errors" ) @@ -106,7 +105,6 @@ func certPool(caFile string, exclusivePool bool) (*x509.CertPool, error) { if !certPool.AppendCertsFromPEM(pem) { return nil, fmt.Errorf("failed to append certificates from PEM file: %q", caFile) } - logrus.Debugf("Trusting %d certs", len(certPool.Subjects())) return certPool, nil }