diff --git a/cli/command/cli.go b/cli/command/cli.go index f27d43a5f738..be2b28dd8840 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -55,6 +55,7 @@ type Cli interface { ServerInfo() ServerInfo NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) DefaultVersion() string + CurrentVersion() string ManifestStore() manifeststore.Store RegistryClient(bool) registryclient.RegistryClient ContentTrustEnabled() bool @@ -68,6 +69,7 @@ type Cli interface { // Instances of the client can be returned from NewDockerCli. type DockerCli struct { configFile *configfile.ConfigFile + options *cliflags.ClientOptions in *streams.In out *streams.Out err io.Writer @@ -75,7 +77,6 @@ type DockerCli struct { serverInfo ServerInfo contentTrust bool contextStore store.Store - currentContext string dockerEndpoint docker.Endpoint contextStoreConfig store.Config initTimeout time.Duration @@ -86,6 +87,15 @@ func (cli *DockerCli) DefaultVersion() string { return api.DefaultVersion } +// CurrentVersion returns the API version currently negotiated, or the default +// version otherwise. +func (cli *DockerCli) CurrentVersion() string { + if cli.client == nil { + return api.DefaultVersion + } + return cli.client.ClientVersion() +} + // Client returns the APIClient func (cli *DockerCli) Client() client.APIClient { return cli.client @@ -216,16 +226,16 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...Initialize } cli.loadConfigFile() + cli.options = opts baseContextStore := store.New(config.ContextStoreDir(), cli.contextStoreConfig) cli.contextStore = &ContextStoreWithDefault{ Store: baseContextStore, Resolver: func() (*DefaultContext, error) { - return ResolveDefaultContext(opts, cli.contextStoreConfig) + return ResolveDefaultContext(cli.options, cli.contextStoreConfig) }, } - cli.currentContext = resolveContextName(opts, cli.configFile) - cli.dockerEndpoint, err = resolveDockerEndpoint(cli.contextStore, cli.currentContext) + cli.dockerEndpoint, err = resolveDockerEndpoint(cli.contextStore, resolveContextName(opts, cli.configFile)) if err != nil { return errors.Wrap(err, "unable to resolve docker endpoint") } @@ -390,7 +400,7 @@ func (cli *DockerCli) ContextStore() store.Store { // CurrentContext does not validate if the given context exists or if it's // valid; errors may occur when trying to use it. func (cli *DockerCli) CurrentContext() string { - return cli.currentContext + return resolveContextName(cli.options, cli.configFile) } // CurrentContext returns the current context name, based on flags, diff --git a/cli/command/container/run.go b/cli/command/container/run.go index 037d580fba92..5cdcffc23d08 100644 --- a/cli/command/container/run.go +++ b/cli/command/container/run.go @@ -111,7 +111,7 @@ func runRun(dockerCli command.Cli, flags *pflag.FlagSet, ropts *runOptions, copt reportError(dockerCli.Err(), "run", err.Error(), true) return cli.StatusError{StatusCode: 125} } - if err = validateAPIVersion(containerConfig, dockerCli.Client().ClientVersion()); err != nil { + if err = validateAPIVersion(containerConfig, dockerCli.CurrentVersion()); err != nil { reportError(dockerCli.Err(), "run", err.Error(), true) return cli.StatusError{StatusCode: 125} } diff --git a/cli/command/context/list.go b/cli/command/context/list.go index 5077157352cd..7798f131ae90 100644 --- a/cli/command/context/list.go +++ b/cli/command/context/list.go @@ -50,26 +50,54 @@ func runList(dockerCli command.Cli, opts *listOptions) error { } var ( curContext = dockerCli.CurrentContext() + curFound bool contexts []*formatter.ClientContext ) for _, rawMeta := range contextMap { isCurrent := rawMeta.Name == curContext + if isCurrent { + curFound = true + } meta, err := command.GetDockerContext(rawMeta) if err != nil { - return err + // Add a stub-entry to the list, including the error-message + // indicating that the context couldn't be loaded. + contexts = append(contexts, &formatter.ClientContext{ + Name: rawMeta.Name, + Current: isCurrent, + Error: err.Error(), + }) + continue } + var errMsg string dockerEndpoint, err := docker.EndpointFromContext(rawMeta) if err != nil { - return err + errMsg = err.Error() } desc := formatter.ClientContext{ Name: rawMeta.Name, Current: isCurrent, Description: meta.Description, DockerEndpoint: dockerEndpoint.Host, + Error: errMsg, } contexts = append(contexts, &desc) } + if !curFound { + // The currently specified context wasn't found. We add a stub-entry + // to the list, including the error-message indicating that the context + // wasn't found. + var errMsg string + _, err := dockerCli.ContextStore().GetMetadata(curContext) + if err != nil { + errMsg = err.Error() + } + contexts = append(contexts, &formatter.ClientContext{ + Name: curContext, + Current: true, + Error: errMsg, + }) + } sort.Slice(contexts, func(i, j int) bool { return sortorder.NaturalLess(contexts[i].Name, contexts[j].Name) }) diff --git a/cli/command/context/list_test.go b/cli/command/context/list_test.go index ef6e6dfe0c34..98ac2e480c55 100644 --- a/cli/command/context/list_test.go +++ b/cli/command/context/list_test.go @@ -39,3 +39,11 @@ func TestListQuiet(t *testing.T) { assert.NilError(t, runList(cli, &listOptions{quiet: true})) golden.Assert(t, cli.OutBuffer().String(), "quiet-list.golden") } + +func TestListError(t *testing.T) { + cli := makeFakeCli(t) + cli.SetCurrentContext("nosuchcontext") + cli.OutBuffer().Reset() + assert.NilError(t, runList(cli, &listOptions{})) + golden.Assert(t, cli.OutBuffer().String(), "list-with-error.golden") +} diff --git a/cli/command/context/show.go b/cli/command/context/show.go index 0bcdfabee16b..bf34bcc5cf6a 100644 --- a/cli/command/context/show.go +++ b/cli/command/context/show.go @@ -25,10 +25,10 @@ func newShowCommand(dockerCli command.Cli) *cobra.Command { func runShow(dockerCli command.Cli) error { context := dockerCli.CurrentContext() - metadata, err := dockerCli.ContextStore().GetMetadata(context) + fmt.Fprintln(dockerCli.Out(), context) + _, err := dockerCli.ContextStore().GetMetadata(context) if err != nil { return err } - fmt.Fprintln(dockerCli.Out(), metadata.Name) return nil } diff --git a/cli/command/context/testdata/list-with-error.golden b/cli/command/context/testdata/list-with-error.golden new file mode 100644 index 000000000000..b9338f412a45 --- /dev/null +++ b/cli/command/context/testdata/list-with-error.golden @@ -0,0 +1,3 @@ +NAME DESCRIPTION DOCKER ENDPOINT ERROR +default Current DOCKER_HOST based configuration unix:///var/run/docker.sock +nosuchcontext * load context "nosuchcontext": context does n… diff --git a/cli/command/context/testdata/list.golden b/cli/command/context/testdata/list.golden index aceb43ec60b1..d9b8121fcf15 100644 --- a/cli/command/context/testdata/list.golden +++ b/cli/command/context/testdata/list.golden @@ -1,5 +1,5 @@ -NAME DESCRIPTION DOCKER ENDPOINT -current * description of current https://someswarmserver.example.com -default Current DOCKER_HOST based configuration unix:///var/run/docker.sock -other description of other https://someswarmserver.example.com -unset description of unset https://someswarmserver.example.com +NAME DESCRIPTION DOCKER ENDPOINT ERROR +current * description of current https://someswarmserver.example.com +default Current DOCKER_HOST based configuration unix:///var/run/docker.sock +other description of other https://someswarmserver.example.com +unset description of unset https://someswarmserver.example.com diff --git a/cli/command/formatter/context.go b/cli/command/formatter/context.go index fd0e67d6f469..c0a36d1ac9b1 100644 --- a/cli/command/formatter/context.go +++ b/cli/command/formatter/context.go @@ -1,8 +1,8 @@ package formatter const ( - // ClientContextTableFormat is the default client context format - ClientContextTableFormat = "table {{.Name}}{{if .Current}} *{{end}}\t{{.Description}}\t{{.DockerEndpoint}}" + // ClientContextTableFormat is the default client context format. + ClientContextTableFormat = "table {{.Name}}{{if .Current}} *{{end}}\t{{.Description}}\t{{.DockerEndpoint}}\t{{.Error}}" dockerEndpointHeader = "DOCKER ENDPOINT" quietContextFormat = "{{.Name}}" @@ -11,10 +11,10 @@ const ( // NewClientContextFormat returns a Format for rendering using a Context func NewClientContextFormat(source string, quiet bool) Format { if quiet { - return Format(quietContextFormat) + return quietContextFormat } if source == TableFormatKey { - return Format(ClientContextTableFormat) + return ClientContextTableFormat } return Format(source) } @@ -25,6 +25,7 @@ type ClientContext struct { Description string DockerEndpoint string Current bool + Error string } // ClientContextWrite writes formatted contexts using the Context @@ -51,6 +52,7 @@ func newClientContextContext() *clientContextContext { "Name": NameHeader, "Description": DescriptionHeader, "DockerEndpoint": dockerEndpointHeader, + "Error": ErrorHeader, } return &ctx } @@ -75,6 +77,12 @@ func (c *clientContextContext) DockerEndpoint() string { return c.c.DockerEndpoint } +// Error returns the truncated error (if any) that occurred when loading the context. +func (c *clientContextContext) Error() string { + // TODO(thaJeztah) add "--no-trunc" option to context ls and set default to 30 cols to match "docker service ps" + return Ellipsis(c.c.Error, 45) +} + // KubernetesEndpoint returns the kubernetes endpoint. // // Deprecated: support for kubernetes endpoints in contexts has been removed, and this formatting option will always be empty. diff --git a/cli/command/formatter/custom.go b/cli/command/formatter/custom.go index 1d78ce62e26c..2d6c979c32f0 100644 --- a/cli/command/formatter/custom.go +++ b/cli/command/formatter/custom.go @@ -16,6 +16,7 @@ const ( StatusHeader = "STATUS" PortsHeader = "PORTS" ImageHeader = "IMAGE" + ErrorHeader = "ERROR" ContainerIDHeader = "CONTAINER ID" ) diff --git a/cli/command/system/version.go b/cli/command/system/version.go index d573681ff704..3ead03215c19 100644 --- a/cli/command/system/version.go +++ b/cli/command/system/version.go @@ -136,7 +136,7 @@ func runVersion(dockerCli command.Cli, opts *versionOptions) error { Client: clientVersion{ Platform: struct{ Name string }{version.PlatformName}, Version: version.Version, - APIVersion: dockerCli.Client().ClientVersion(), + APIVersion: dockerCli.CurrentVersion(), DefaultAPIVersion: dockerCli.DefaultVersion(), GoVersion: runtime.Version(), GitCommit: version.GitCommit, diff --git a/cli/command/task/formatter.go b/cli/command/task/formatter.go index 0958a1e89e62..13e9579be198 100644 --- a/cli/command/task/formatter.go +++ b/cli/command/task/formatter.go @@ -61,7 +61,7 @@ func FormatWrite(ctx formatter.Context, tasks []swarm.Task, names map[string]str "Node": nodeHeader, "DesiredState": desiredStateHeader, "CurrentState": currentStateHeader, - "Error": errorHeader, + "Error": formatter.ErrorHeader, "Ports": formatter.PortsHeader, } return ctx.Write(&taskCtx, render) @@ -124,11 +124,11 @@ func (c *taskContext) CurrentState() string { func (c *taskContext) Error() string { // Trim and quote the error message. taskErr := c.task.Status.Err - if c.trunc && len(taskErr) > maxErrLength { - taskErr = fmt.Sprintf("%s…", taskErr[:maxErrLength-1]) + if c.trunc { + taskErr = formatter.Ellipsis(taskErr, maxErrLength) } if len(taskErr) > 0 { - taskErr = fmt.Sprintf("\"%s\"", taskErr) + taskErr = fmt.Sprintf(`"%s"`, taskErr) } return taskErr } diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go index c175ef64cb31..0105c7fb5850 100644 --- a/cmd/docker/docker.go +++ b/cmd/docker/docker.go @@ -14,7 +14,6 @@ import ( cliflags "github.com/docker/cli/cli/flags" "github.com/docker/cli/cli/version" "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/client" "github.com/moby/buildkit/util/appcontext" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -274,7 +273,7 @@ func main() { } type versionDetails interface { - Client() client.APIClient + CurrentVersion() string ServerInfo() command.ServerInfo } @@ -333,7 +332,7 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) error { return false } } - versionOlderThan = func(v string) bool { return versions.LessThan(details.Client().ClientVersion(), v) } + versionOlderThan = func(v string) bool { return versions.LessThan(details.CurrentVersion(), v) } ) cmd.Flags().VisitAll(func(f *pflag.Flag) { @@ -390,8 +389,8 @@ func areFlagsSupported(cmd *cobra.Command, details versionDetails) error { if !f.Changed { return } - if !isVersionSupported(f, details.Client().ClientVersion()) { - errs = append(errs, fmt.Sprintf(`"--%s" requires API version %s, but the Docker daemon API version is %s`, f.Name, getFlagAnnotation(f, "version"), details.Client().ClientVersion())) + if !isVersionSupported(f, details.CurrentVersion()) { + errs = append(errs, fmt.Sprintf(`"--%s" requires API version %s, but the Docker daemon API version is %s`, f.Name, getFlagAnnotation(f, "version"), details.CurrentVersion())) return } if !isOSTypeSupported(f, details.ServerInfo().OSType) { @@ -417,11 +416,11 @@ func areFlagsSupported(cmd *cobra.Command, details versionDetails) error { func areSubcommandsSupported(cmd *cobra.Command, details versionDetails) error { // Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack` for curr := cmd; curr != nil; curr = curr.Parent() { - if cmdVersion, ok := curr.Annotations["version"]; ok && versions.LessThan(details.Client().ClientVersion(), cmdVersion) { - return fmt.Errorf("%s requires API version %s, but the Docker daemon API version is %s", cmd.CommandPath(), cmdVersion, details.Client().ClientVersion()) + if cmdVersion, ok := curr.Annotations["version"]; ok && versions.LessThan(details.CurrentVersion(), cmdVersion) { + return fmt.Errorf("%s requires API version %s, but the Docker daemon API version is %s", cmd.CommandPath(), cmdVersion, details.CurrentVersion()) } - if os, ok := curr.Annotations["ostype"]; ok && os != details.ServerInfo().OSType { - return fmt.Errorf("%s is only supported on a Docker daemon running on %s, but the Docker daemon is running on %s", cmd.CommandPath(), os, details.ServerInfo().OSType) + if ost, ok := curr.Annotations["ostype"]; ok && ost != details.ServerInfo().OSType { + return fmt.Errorf("%s is only supported on a Docker daemon running on %s, but the Docker daemon is running on %s", cmd.CommandPath(), ost, details.ServerInfo().OSType) } if _, ok := curr.Annotations["experimental"]; ok && !details.ServerInfo().HasExperimental { return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath()) diff --git a/e2e/context/testdata/context-ls-notls.golden b/e2e/context/testdata/context-ls-notls.golden index 2d33df13a4f0..c36c8e7bb6da 100644 --- a/e2e/context/testdata/context-ls-notls.golden +++ b/e2e/context/testdata/context-ls-notls.golden @@ -1,3 +1,3 @@ -NAME DESCRIPTION DOCKER ENDPOINT -default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock -remote my remote cluster ssh://someserver +NAME DESCRIPTION DOCKER ENDPOINT ERROR +default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock +remote my remote cluster ssh://someserver diff --git a/e2e/context/testdata/context-ls-tls.golden b/e2e/context/testdata/context-ls-tls.golden index 13aed65a881d..6d9756e7e57b 100644 --- a/e2e/context/testdata/context-ls-tls.golden +++ b/e2e/context/testdata/context-ls-tls.golden @@ -1,3 +1,3 @@ -NAME DESCRIPTION DOCKER ENDPOINT -default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock -test unix:///var/run/docker.sock +NAME DESCRIPTION DOCKER ENDPOINT ERROR +default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock +test unix:///var/run/docker.sock diff --git a/e2e/context/testdata/context-ls.golden b/e2e/context/testdata/context-ls.golden index 2d33df13a4f0..c36c8e7bb6da 100644 --- a/e2e/context/testdata/context-ls.golden +++ b/e2e/context/testdata/context-ls.golden @@ -1,3 +1,3 @@ -NAME DESCRIPTION DOCKER ENDPOINT -default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock -remote my remote cluster ssh://someserver +NAME DESCRIPTION DOCKER ENDPOINT ERROR +default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock +remote my remote cluster ssh://someserver diff --git a/internal/test/cli.go b/internal/test/cli.go index 62bf6351e77b..db800bec8cdd 100644 --- a/internal/test/cli.go +++ b/internal/test/cli.go @@ -101,6 +101,11 @@ func (c *FakeCli) Client() client.APIClient { return c.client } +// CurrentVersion returns the API version used by FakeCli. +func (c *FakeCli) CurrentVersion() string { + return c.DefaultVersion() +} + // Out returns the output stream (stdout) the cli should write on func (c *FakeCli) Out() *streams.Out { return c.out