From 3b7235edca58203aa9a857bdf031ca39e8a686ed Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 4 Nov 2022 15:47:14 +0100 Subject: [PATCH] cli/command: initialize client and load content lazily This allows commands that don't require a client connection (such as `context use`) to be functional, but still produces an error when trying to run a command that needs to connect with the API; mkdir -p ~/.docker/ && echo '{"currentContext":"nosuchcontext"}' > ~/.docker/config.json docker version Failed to initialize: unable to resolve docker endpoint: load context "nosuchcontext": context does not exist: open /root/.docker/contexts/meta/8bfef2a74c7d06add4bf4c73b0af97d9f79c76fe151ae0e18b9d7e57104c149b/meta.json: no such file or directory docker context use default default Current context is now "default" docker version Client: Version: 22.06.0-dev API version: 1.42 ... Signed-off-by: Sebastiaan van Stijn --- cli/command/cli.go | 58 ++++++++++++++++++++++++++++++----------- cli/command/cli_test.go | 6 +++-- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/cli/command/cli.go b/cli/command/cli.go index 0cce3fa505..2ac7a6698b 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -2,12 +2,14 @@ package command import ( "context" + "fmt" "io" "os" "path/filepath" "runtime" "strconv" "strings" + "sync" "time" "github.com/docker/cli/cli/config" @@ -78,6 +80,8 @@ type DockerCli struct { contentTrust bool contextStore store.Store currentContext string + init sync.Once + initErr error dockerEndpoint docker.Endpoint contextStoreConfig store.Config initTimeout time.Duration @@ -91,6 +95,7 @@ func (cli *DockerCli) DefaultVersion() string { // CurrentVersion returns the API version currently negotiated, or the default // version otherwise. func (cli *DockerCli) CurrentVersion() string { + _ = cli.initialize() if cli.client == nil { return api.DefaultVersion } @@ -99,6 +104,10 @@ func (cli *DockerCli) CurrentVersion() string { // Client returns the APIClient func (cli *DockerCli) Client() client.APIClient { + if err := cli.initialize(); err != nil { + _, _ = fmt.Fprintf(cli.Err(), "Failed to initialize: %s\n", err) + os.Exit(1) + } return cli.client } @@ -203,8 +212,6 @@ func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClien // Initialize the dockerCli runs initialization that must happen after command // line flags are parsed. func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...InitializeOpt) error { - var err error - for _, o := range ops { if err := o(cli); err != nil { return err @@ -232,18 +239,6 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...Initialize return ResolveDefaultContext(cli.options, cli.contextStoreConfig) }, } - cli.dockerEndpoint, err = resolveDockerEndpoint(cli.contextStore, resolveContextName(opts, cli.configFile)) - if err != nil { - return errors.Wrap(err, "unable to resolve docker endpoint") - } - - if cli.client == nil { - cli.client, err = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile) - if err != nil { - return err - } - } - cli.initializeFromClient() return nil } @@ -282,6 +277,9 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF } func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) { + if s == nil { + return docker.Endpoint{}, fmt.Errorf("no context store initialized") + } ctxMeta, err := s.GetMetadata(contextName) if err != nil { return docker.Endpoint{}, err @@ -331,7 +329,7 @@ func (cli *DockerCli) getInitTimeout() time.Duration { func (cli *DockerCli) initializeFromClient() { ctx := context.Background() - if !strings.HasPrefix(cli.DockerEndpoint().Host, "ssh://") { + if !strings.HasPrefix(cli.dockerEndpoint.Host, "ssh://") { // @FIXME context.WithTimeout doesn't work with connhelper / ssh connections // time="2020-04-10T10:16:26Z" level=warning msg="commandConn.CloseWrite: commandconn: failed to wait: signal: killed" var cancel func() @@ -428,9 +426,39 @@ func resolveContextName(opts *cliflags.ClientOptions, config *configfile.ConfigF // DockerEndpoint returns the current docker endpoint func (cli *DockerCli) DockerEndpoint() docker.Endpoint { + if err := cli.initialize(); err != nil { + // Note that we're not terminating here, as this function may be used + // in cases where we're able to continue. + _, _ = fmt.Fprintf(cli.Err(), "%v\n", cli.initErr) + } return cli.dockerEndpoint } +func (cli *DockerCli) getDockerEndPoint() (ep docker.Endpoint, err error) { + cn := cli.CurrentContext() + if cn == DefaultContextName { + return resolveDefaultDockerEndpoint(cli.options) + } + return resolveDockerEndpoint(cli.contextStore, cn) +} + +func (cli *DockerCli) initialize() error { + cli.init.Do(func() { + cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint() + if cli.initErr != nil { + cli.initErr = errors.Wrap(cli.initErr, "unable to resolve docker endpoint") + return + } + if cli.client == nil { + if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil { + return + } + } + cli.initializeFromClient() + }) + return cli.initErr +} + // Apply all the operation on the cli func (cli *DockerCli) Apply(ops ...DockerCliOption) error { for _, op := range ops { diff --git a/cli/command/cli_test.go b/cli/command/cli_test.go index dcd9103fb6..7a0b4e727e 100644 --- a/cli/command/cli_test.go +++ b/cli/command/cli_test.go @@ -118,7 +118,7 @@ func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) { } func TestInitializeFromClient(t *testing.T) { - defaultVersion := "v1.55" + const defaultVersion = "v1.55" testcases := []struct { doc string @@ -160,7 +160,8 @@ func TestInitializeFromClient(t *testing.T) { } cli := &DockerCli{client: apiclient} - cli.initializeFromClient() + err := cli.Initialize(flags.NewClientOptions()) + assert.NilError(t, err) assert.DeepEqual(t, cli.ServerInfo(), testcase.expectedServer) assert.Equal(t, apiclient.negotiated, testcase.negotiated) }) @@ -202,6 +203,7 @@ func TestInitializeFromClientHangs(t *testing.T) { cli := &DockerCli{client: apiClient, initTimeout: time.Millisecond} err := cli.Initialize(flags.NewClientOptions()) assert.Check(t, err) + cli.CurrentVersion() close(initializedCh) }()