Skip to content

Commit 7992dc3

Browse files
committed
Introduce docker context store
This PR adds a store to the CLI, that can be leveraged to persist and retrieve credentials for various API endpoints, as well as context-specific settings (initially, default stack orchestrator, but we could expand that). This comes with the logic to persist and retrieve endpoints configs for both Docker and Kubernetes APIs. Signed-off-by: Simon Ferquel <simon.ferquel@docker.com>
1 parent 3a6f8b6 commit 7992dc3

29 files changed

+2153
-413
lines changed

cli/command/cli.go

Lines changed: 175 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,25 @@ package command
33
import (
44
"context"
55
"io"
6-
"net"
7-
"net/http"
86
"os"
97
"path/filepath"
108
"runtime"
119
"strconv"
12-
"time"
1310

1411
"github.com/docker/cli/cli"
1512
"github.com/docker/cli/cli/config"
1613
cliconfig "github.com/docker/cli/cli/config"
1714
"github.com/docker/cli/cli/config/configfile"
18-
"github.com/docker/cli/cli/connhelper"
15+
dcontext "github.com/docker/cli/cli/context"
16+
"github.com/docker/cli/cli/context/docker"
17+
kubcontext "github.com/docker/cli/cli/context/kubernetes"
18+
"github.com/docker/cli/cli/context/store"
1919
cliflags "github.com/docker/cli/cli/flags"
2020
manifeststore "github.com/docker/cli/cli/manifest/store"
2121
registryclient "github.com/docker/cli/cli/registry/client"
2222
"github.com/docker/cli/cli/trust"
2323
dopts "github.com/docker/cli/opts"
2424
clitypes "github.com/docker/cli/types"
25-
"github.com/docker/docker/api"
2625
"github.com/docker/docker/api/types"
2726
registrytypes "github.com/docker/docker/api/types/registry"
2827
"github.com/docker/docker/client"
@@ -34,6 +33,9 @@ import (
3433
"github.com/theupdateframework/notary/passphrase"
3534
)
3635

36+
// ContextDockerHost is the reported context when DOCKER_HOST env var or -H flag is set
37+
const ContextDockerHost = "<DOCKER_HOST>"
38+
3739
// Streams is an interface which exposes the standard input and output streams
3840
type Streams interface {
3941
In() *InStream
@@ -57,6 +59,9 @@ type Cli interface {
5759
RegistryClient(bool) registryclient.RegistryClient
5860
ContentTrustEnabled() bool
5961
NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error)
62+
ContextStore() store.Store
63+
CurrentContext() string
64+
StackOrchestrator(flagValue string) (Orchestrator, error)
6065
}
6166

6267
// DockerCli is an instance the docker command line client.
@@ -71,8 +76,16 @@ type DockerCli struct {
7176
clientInfo ClientInfo
7277
contentTrust bool
7378
newContainerizeClient func(string) (clitypes.ContainerizedClient, error)
79+
contextStore store.Store
80+
currentContext string
7481
}
7582

83+
var storeConfig = store.NewConfig(
84+
func() interface{} { return &DockerContext{} },
85+
store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }),
86+
store.EndpointTypeGetter(kubcontext.KubernetesEndpoint, func() interface{} { return &kubcontext.EndpointMeta{} }),
87+
)
88+
7689
// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
7790
func (cli *DockerCli) DefaultVersion() string {
7891
return cli.clientInfo.DefaultVersion
@@ -167,14 +180,23 @@ func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.Registry
167180
// line flags are parsed.
168181
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
169182
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
170-
171183
var err error
172-
cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
184+
cli.contextStore = store.New(cliconfig.ContextStoreDir(), storeConfig)
185+
cli.currentContext, err = resolveContextName(opts.Common, cli.configFile)
186+
if err != nil {
187+
return err
188+
}
189+
endpoint, err := resolveDockerEndpoint(cli.contextStore, cli.currentContext, opts.Common)
190+
if err != nil {
191+
return errors.Wrap(err, "unable to resolve docker endpoint")
192+
}
193+
194+
cli.client, err = newAPIClientFromEndpoint(endpoint, cli.configFile)
173195
if tlsconfig.IsErrEncryptedKey(err) {
174196
passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
175197
newClient := func(password string) (client.APIClient, error) {
176-
opts.Common.TLSOptions.Passphrase = password
177-
return NewAPIClientFromFlags(opts.Common, cli.configFile)
198+
endpoint.TLSPassword = password
199+
return newAPIClientFromEndpoint(endpoint, cli.configFile)
178200
}
179201
cli.client, err = getClientWithPassword(passRetriever, newClient)
180202
}
@@ -198,6 +220,75 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
198220
return nil
199221
}
200222

223+
// NewAPIClientFromFlags creates a new APIClient from command line flags
224+
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
225+
store := store.New(cliconfig.ContextStoreDir(), storeConfig)
226+
contextName, err := resolveContextName(opts, configFile)
227+
if err != nil {
228+
return nil, err
229+
}
230+
endpoint, err := resolveDockerEndpoint(store, contextName, opts)
231+
if err != nil {
232+
return nil, errors.Wrap(err, "unable to resolve docker endpoint")
233+
}
234+
return newAPIClientFromEndpoint(endpoint, configFile)
235+
}
236+
237+
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
238+
clientOpts, err := ep.ClientOpts()
239+
if err != nil {
240+
return nil, err
241+
}
242+
customHeaders := configFile.HTTPHeaders
243+
if customHeaders == nil {
244+
customHeaders = map[string]string{}
245+
}
246+
customHeaders["User-Agent"] = UserAgent()
247+
clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))
248+
return client.NewClientWithOpts(clientOpts...)
249+
}
250+
251+
func resolveDockerEndpoint(s store.Store, contextName string, opts *cliflags.CommonOptions) (docker.Endpoint, error) {
252+
if contextName != ContextDockerHost {
253+
ctxMeta, err := s.GetContextMetadata(contextName)
254+
if err != nil {
255+
return docker.Endpoint{}, err
256+
}
257+
epMeta, err := docker.EndpointFromContext(ctxMeta)
258+
if err != nil {
259+
return docker.Endpoint{}, err
260+
}
261+
return epMeta.WithTLSData(s, contextName)
262+
}
263+
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
264+
if err != nil {
265+
return docker.Endpoint{}, err
266+
}
267+
268+
var (
269+
skipTLSVerify bool
270+
tlsData *dcontext.TLSData
271+
)
272+
273+
if opts.TLSOptions != nil {
274+
skipTLSVerify = opts.TLSOptions.InsecureSkipVerify
275+
tlsData, err = dcontext.TLSDataFromFiles(opts.TLSOptions.CAFile, opts.TLSOptions.CertFile, opts.TLSOptions.KeyFile)
276+
if err != nil {
277+
return docker.Endpoint{}, err
278+
}
279+
}
280+
281+
return docker.Endpoint{
282+
EndpointMeta: docker.EndpointMeta{
283+
EndpointMetaBase: dcontext.EndpointMetaBase{
284+
Host: host,
285+
SkipTLSVerify: skipTLSVerify,
286+
},
287+
},
288+
TLSData: tlsData,
289+
}, nil
290+
}
291+
201292
func isEnabled(value string) (bool, error) {
202293
switch value {
203294
case "enabled":
@@ -253,6 +344,51 @@ func (cli *DockerCli) NewContainerizedEngineClient(sockPath string) (clitypes.Co
253344
return cli.newContainerizeClient(sockPath)
254345
}
255346

347+
// ContextStore returns the ContextStore
348+
func (cli *DockerCli) ContextStore() store.Store {
349+
return cli.contextStore
350+
}
351+
352+
// CurrentContext returns the current context name
353+
func (cli *DockerCli) CurrentContext() string {
354+
return cli.currentContext
355+
}
356+
357+
// StackOrchestrator resolves which stack orchestrator is in use
358+
func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error) {
359+
var ctxOrchestrator string
360+
361+
configFile := cli.configFile
362+
if configFile == nil {
363+
configFile = cliconfig.LoadDefaultConfigFile(cli.Err())
364+
}
365+
366+
currentContext := cli.CurrentContext()
367+
if currentContext == "" {
368+
currentContext = configFile.CurrentContext
369+
}
370+
if currentContext == "" {
371+
currentContext = ContextDockerHost
372+
}
373+
if currentContext != ContextDockerHost {
374+
contextstore := cli.contextStore
375+
if contextstore == nil {
376+
contextstore = store.New(cliconfig.ContextStoreDir(), storeConfig)
377+
}
378+
ctxRaw, err := contextstore.GetContextMetadata(currentContext)
379+
if err != nil {
380+
return "", err
381+
}
382+
ctxMeta, err := GetDockerContext(ctxRaw)
383+
if err != nil {
384+
return "", err
385+
}
386+
ctxOrchestrator = string(ctxMeta.StackOrchestrator)
387+
}
388+
389+
return GetStackOrchestrator(flagValue, ctxOrchestrator, configFile.StackOrchestrator, cli.Err())
390+
}
391+
256392
// ServerInfo stores details about the supported features and platform of the
257393
// server
258394
type ServerInfo struct {
@@ -272,51 +408,6 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool, containe
272408
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err, contentTrust: isTrusted, newContainerizeClient: containerizedFn}
273409
}
274410

275-
// NewAPIClientFromFlags creates a new APIClient from command line flags
276-
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
277-
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
278-
if err != nil {
279-
return &client.Client{}, err
280-
}
281-
var clientOpts []func(*client.Client) error
282-
helper, err := connhelper.GetConnectionHelper(host)
283-
if err != nil {
284-
return &client.Client{}, err
285-
}
286-
if helper == nil {
287-
clientOpts = append(clientOpts, withHTTPClient(opts.TLSOptions))
288-
clientOpts = append(clientOpts, client.WithHost(host))
289-
} else {
290-
clientOpts = append(clientOpts, func(c *client.Client) error {
291-
httpClient := &http.Client{
292-
// No tls
293-
// No proxy
294-
Transport: &http.Transport{
295-
DialContext: helper.Dialer,
296-
},
297-
}
298-
return client.WithHTTPClient(httpClient)(c)
299-
})
300-
clientOpts = append(clientOpts, client.WithHost(helper.Host))
301-
clientOpts = append(clientOpts, client.WithDialContext(helper.Dialer))
302-
}
303-
304-
customHeaders := configFile.HTTPHeaders
305-
if customHeaders == nil {
306-
customHeaders = map[string]string{}
307-
}
308-
customHeaders["User-Agent"] = UserAgent()
309-
clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))
310-
311-
verStr := api.DefaultVersion
312-
if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" {
313-
verStr = tmpStr
314-
}
315-
clientOpts = append(clientOpts, client.WithVersion(verStr))
316-
317-
return client.NewClientWithOpts(clientOpts...)
318-
}
319-
320411
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
321412
var host string
322413
switch len(hosts) {
@@ -331,35 +422,37 @@ func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error
331422
return dopts.ParseHost(tlsOptions != nil, host)
332423
}
333424

334-
func withHTTPClient(tlsOpts *tlsconfig.Options) func(*client.Client) error {
335-
return func(c *client.Client) error {
336-
if tlsOpts == nil {
337-
// Use the default HTTPClient
338-
return nil
339-
}
340-
341-
opts := *tlsOpts
342-
opts.ExclusiveRootPools = true
343-
tlsConfig, err := tlsconfig.Client(opts)
344-
if err != nil {
345-
return err
346-
}
347-
348-
httpClient := &http.Client{
349-
Transport: &http.Transport{
350-
TLSClientConfig: tlsConfig,
351-
DialContext: (&net.Dialer{
352-
KeepAlive: 30 * time.Second,
353-
Timeout: 30 * time.Second,
354-
}).DialContext,
355-
},
356-
CheckRedirect: client.CheckRedirect,
357-
}
358-
return client.WithHTTPClient(httpClient)(c)
359-
}
360-
}
361-
362425
// UserAgent returns the user agent string used for making API requests
363426
func UserAgent() string {
364427
return "Docker-Client/" + cli.Version + " (" + runtime.GOOS + ")"
365428
}
429+
430+
// resolveContextName resolves the current context name with the following rules:
431+
// - setting both --context and --host flags is ambiguous
432+
// - if --context is set, use this value
433+
// - if --host flag or DOCKER_HOST is set, fallbacks to use the same logic as before context-store was added
434+
// for backward compatibility with existing scripts
435+
// - if DOCKER_CONTEXT is set, use this value
436+
// - if Config file has a globally set "CurrentContext", use this value
437+
// - fallbacks to default HOST, uses TLS config from flags/env vars
438+
func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigFile) (string, error) {
439+
if opts.Context != "" && len(opts.Hosts) > 0 {
440+
return "", errors.New("Conflicting options: either specify --host or --context, not bot")
441+
}
442+
if opts.Context != "" {
443+
return opts.Context, nil
444+
}
445+
if len(opts.Hosts) > 0 {
446+
return ContextDockerHost, nil
447+
}
448+
if _, present := os.LookupEnv("DOCKER_HOST"); present {
449+
return ContextDockerHost, nil
450+
}
451+
if ctxName, ok := os.LookupEnv("DOCKER_CONTEXT"); ok {
452+
return ctxName, nil
453+
}
454+
if config != nil && config.CurrentContext != "" {
455+
return config.CurrentContext, nil
456+
}
457+
return ContextDockerHost, nil
458+
}

cli/command/cli_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
6666
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
6767
customVersion := "v3.3.3"
6868
defer env.Patch(t, "DOCKER_API_VERSION", customVersion)()
69+
defer env.Patch(t, "DOCKER_HOST", ":2375")()
6970

7071
opts := &flags.CommonOptions{}
7172
configFile := &configfile.ConfigFile{}

cli/command/context.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package command
2+
3+
import (
4+
"errors"
5+
6+
"github.com/docker/cli/cli/context/store"
7+
)
8+
9+
// DockerContext is a typed representation of what we put in Context metadata
10+
type DockerContext struct {
11+
Description string `json:"description,omitempty"`
12+
StackOrchestrator Orchestrator `json:"stack_orchestrator,omitempty"`
13+
}
14+
15+
// GetDockerContext extracts metadata from stored context metadata
16+
func GetDockerContext(storeMetadata store.ContextMetadata) (DockerContext, error) {
17+
if storeMetadata.Metadata == nil {
18+
// can happen if we save endpoints before assigning a context metadata
19+
// it is totally valid, and we should return a default initialized value
20+
return DockerContext{}, nil
21+
}
22+
res, ok := storeMetadata.Metadata.(DockerContext)
23+
if !ok {
24+
return DockerContext{}, errors.New("context metadata is not a valid DockerContext")
25+
}
26+
return res, nil
27+
}

0 commit comments

Comments
 (0)