From ed0511251db91e632dfca980b12982a249227acc Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 1 Apr 2025 23:23:40 +0200 Subject: [PATCH] cli/command: DockerCli.Initialize: make sure context-store config is set In most situations, the CLI is created through the `NewDockerCli` constructor, however, it's possible to construct a CLI manually (`&DockerCli{}`). We should probably prevent this (and un-export the `DockerCli` implementation), but currently have some code-paths that depend on the type being exported. When constructing the CLI with this approach, the CLI would not be fully initialized and not have the context-store configuration set up. Using the default context store without a config set will result in Endpoints from contexts not being type-mapped correctly, and used as a generic `map[string]any`, instead of a [docker.EndpointMeta]. When looking up the API endpoint (using [EndpointFromContext]), no endpoint will be found, and a default, empty endpoint will be used instead which in its turn, causes [newAPIClientFromEndpoint] to be initialized with the default config instead of settings for the current context (which may mean; connecting with the wrong endpoint and/or TLS Config to be missing). I'm not sure if this situation could happen in practice, but it caused some of our unit-tests ([TestInitializeFromClient] among others) to fail when running outside of the dev-container on a host that used Docker Desktop's "desktop-linux" context. In that situation, the test would produce the wrong "Ping" results (using defaults, instead of the results produced in the test). This patch: - updates the contextStoreConfig field to be a pointer, so that we are able to detect if a config was already set. - updates the `Initialize` function to set the default context-store config if no config was found (technically the field is mostly immutable, and can only set through `WithDefaultContextStoreConfig`, so this may be slightly redundant). We should update this code to be less error-prone to use; the combination of an exported type (`DockerCli`), a constructor `NewDockerCli` and a `Initialize` function (as well as some internal contructors to allow lazy initialization) make constructing the "CLI" hard to use, and there's various codepaths where it can be in a partially initialized state. The same applies to the default context store, which also requires too much "domain" knowledge to use properly. I'm leaving improvements around that for a follow-up. [EndpointFromContext]: https://github.com/docker/cli/blob/33494921b80fd0b5a06acc3a34fa288de4bb2e6b/cli/context/docker/load.go#L139-L149 [docker.EndpointMeta]: https://github.com/docker/cli/blob/33494921b80fd0b5a06acc3a34fa288de4bb2e6b/cli/context/docker/load.go#L19-L21 [newAPIClientFromEndpoint]: https://github.com/docker/cli/blob/33494921b80fd0b5a06acc3a34fa288de4bb2e6b/cli/command/cli.go#L295-L305 [TestInitializeFromClient]: https://github.com/docker/cli/blob/33494921b80fd0b5a06acc3a34fa288de4bb2e6b/cli/command/cli_test.go#L157-L205 Signed-off-by: Sebastiaan van Stijn --- cli/command/cli.go | 30 ++++++++++++++++++++++++++---- cli/command/cli_options.go | 3 ++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/cli/command/cli.go b/cli/command/cli.go index 2a78c5079c..3fcf67deb7 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -59,7 +59,9 @@ type Cli interface { } // DockerCli is an instance the docker command line client. -// Instances of the client can be returned from NewDockerCli. +// Instances of the client should be created using the [NewDockerCli] +// constructor to make sure they are properly initialized with defaults +// set. type DockerCli struct { configFile *configfile.ConfigFile options *cliflags.ClientOptions @@ -74,7 +76,7 @@ type DockerCli struct { init sync.Once initErr error dockerEndpoint docker.Endpoint - contextStoreConfig store.Config + contextStoreConfig *store.Config initTimeout time.Duration res telemetryResource @@ -250,13 +252,33 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) return errors.New("conflicting options: cannot specify both --host and --context") } + if cli.contextStoreConfig == nil { + // This path can be hit when calling Initialize on a DockerCli that's + // not constructed through [NewDockerCli]. Using the default context + // store without a config set will result in Endpoints from contexts + // not being type-mapped correctly, and used as a generic "map[string]any", + // instead of a [docker.EndpointMeta]. + // + // When looking up the API endpoint (using [EndpointFromContext]), no + // endpoint will be found, and a default, empty endpoint will be used + // instead which in its turn, causes newAPIClientFromEndpoint to + // be initialized with the default config instead of settings for + // the current context (which may mean; connecting with the wrong + // endpoint and/or TLS Config to be missing). + // + // [EndpointFromContext]: https://github.com/docker/cli/blob/33494921b80fd0b5a06acc3a34fa288de4bb2e6b/cli/context/docker/load.go#L139-L149 + if err := WithDefaultContextStoreConfig()(cli); err != nil { + return err + } + } + cli.options = opts cli.configFile = config.LoadDefaultConfigFile(cli.err) cli.currentContext = resolveContextName(cli.options, cli.configFile) cli.contextStore = &ContextStoreWithDefault{ - Store: store.New(config.ContextStoreDir(), cli.contextStoreConfig), + Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig), Resolver: func() (*DefaultContext, error) { - return ResolveDefaultContext(cli.options, cli.contextStoreConfig) + return ResolveDefaultContext(cli.options, *cli.contextStoreConfig) }, } diff --git a/cli/command/cli_options.go b/cli/command/cli_options.go index ef133d6a9a..ff6506f042 100644 --- a/cli/command/cli_options.go +++ b/cli/command/cli_options.go @@ -101,7 +101,8 @@ func WithContentTrust(enabled bool) CLIOption { // WithDefaultContextStoreConfig configures the cli to use the default context store configuration. func WithDefaultContextStoreConfig() CLIOption { return func(cli *DockerCli) error { - cli.contextStoreConfig = DefaultContextStoreConfig() + cfg := DefaultContextStoreConfig() + cli.contextStoreConfig = &cfg return nil } }