Skip to content

Commit 5e36ccf

Browse files
committed
cli/command: load context, and initialize client 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 <github@gone.nl>
1 parent 3e4e0eb commit 5e36ccf

2 files changed

Lines changed: 39 additions & 16 deletions

File tree

cli/command/cli.go

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package command
22

33
import (
44
"context"
5+
"fmt"
56
"io"
67
"os"
78
"path/filepath"
89
"runtime"
910
"strconv"
1011
"strings"
12+
"sync"
1113
"time"
1214

1315
"github.com/docker/cli/cli/config"
@@ -77,6 +79,8 @@ type DockerCli struct {
7779
serverInfo ServerInfo
7880
contentTrust bool
7981
contextStore store.Store
82+
init sync.Once
83+
initErr error
8084
dockerEndpoint docker.Endpoint
8185
contextStoreConfig store.Config
8286
initTimeout time.Duration
@@ -90,14 +94,18 @@ func (cli *DockerCli) DefaultVersion() string {
9094
// CurrentVersion returns the API version currently negotiated, or the default
9195
// version otherwise.
9296
func (cli *DockerCli) CurrentVersion() string {
93-
if cli.client == nil {
97+
if err := cli.initialize(); err != nil {
9498
return api.DefaultVersion
9599
}
96100
return cli.client.ClientVersion()
97101
}
98102

99103
// Client returns the APIClient
100104
func (cli *DockerCli) Client() client.APIClient {
105+
if err := cli.initialize(); err != nil {
106+
_, _ = fmt.Fprintf(cli.Err(), "Failed to initialize: %s\n", err)
107+
os.Exit(1)
108+
}
101109
return cli.client
102110
}
103111

@@ -205,8 +213,6 @@ func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClien
205213
// Initialize the dockerCli runs initialization that must happen after command
206214
// line flags are parsed.
207215
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...InitializeOpt) error {
208-
var err error
209-
210216
for _, o := range ops {
211217
if err := o(cli); err != nil {
212218
return err
@@ -235,18 +241,6 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...Initialize
235241
return ResolveDefaultContext(cli.options, cli.contextStoreConfig)
236242
},
237243
}
238-
cli.dockerEndpoint, err = resolveDockerEndpoint(cli.contextStore, resolveContextName(opts, cli.configFile))
239-
if err != nil {
240-
return errors.Wrap(err, "unable to resolve docker endpoint")
241-
}
242-
243-
if cli.client == nil {
244-
cli.client, err = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile)
245-
if err != nil {
246-
return err
247-
}
248-
}
249-
cli.initializeFromClient()
250244
return nil
251245
}
252246

@@ -285,6 +279,9 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
285279
}
286280

287281
func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) {
282+
if s == nil {
283+
return docker.Endpoint{}, fmt.Errorf("no context store initialized")
284+
}
288285
ctxMeta, err := s.GetMetadata(contextName)
289286
if err != nil {
290287
return docker.Endpoint{}, err
@@ -433,9 +430,31 @@ func resolveContextName(opts *cliflags.ClientOptions, config *configfile.ConfigF
433430
//
434431
// TODO(thaJeztah): deprecate this in favor or cli.Client().Host? (unless someone needs the TLSConfig).
435432
func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
433+
if err := cli.initialize(); err != nil {
434+
// Note that we're not terminating here, as this function may be used
435+
// in cases where we're able to continue.
436+
_, _ = fmt.Fprintf(cli.Err(), "Failed to initialize: %s\n", cli.initErr)
437+
}
436438
return cli.dockerEndpoint
437439
}
438440

441+
func (cli *DockerCli) initialize() error {
442+
cli.init.Do(func() {
443+
cli.dockerEndpoint, cli.initErr = resolveDockerEndpoint(cli.contextStore, resolveContextName(cli.options, cli.configFile))
444+
if cli.initErr != nil {
445+
cli.initErr = errors.Wrap(cli.initErr, "unable to resolve docker endpoint")
446+
return
447+
}
448+
if cli.client == nil {
449+
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil {
450+
return
451+
}
452+
}
453+
cli.initializeFromClient()
454+
})
455+
return cli.initErr
456+
}
457+
439458
// Apply all the operation on the cli
440459
func (cli *DockerCli) Apply(ops ...DockerCliOption) error {
441460
for _, op := range ops {

cli/command/cli_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,10 @@ func TestInitializeFromClient(t *testing.T) {
159159
version: defaultVersion,
160160
}
161161

162-
cli := &DockerCli{client: apiclient}
162+
cli := &DockerCli{
163+
client: apiclient,
164+
err: io.Discard,
165+
}
163166
cli.initializeFromClient()
164167
assert.DeepEqual(t, cli.ServerInfo(), testcase.expectedServer)
165168
assert.Equal(t, apiclient.negotiated, testcase.negotiated)
@@ -201,6 +204,7 @@ func TestInitializeFromClientHangs(t *testing.T) {
201204
go func() {
202205
cli := &DockerCli{client: apiClient, initTimeout: time.Millisecond}
203206
cli.Initialize(flags.NewClientOptions())
207+
cli.CurrentVersion()
204208
close(initializedCh)
205209
}()
206210

0 commit comments

Comments
 (0)