Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 183 additions & 82 deletions cli/command/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,25 @@ package command
import (
"context"
"io"
"net"
"net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"time"

"github.com/docker/cli/cli"
"github.com/docker/cli/cli/config"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/connhelper"
dcontext "github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/docker"
kubcontext "github.com/docker/cli/cli/context/kubernetes"
"github.com/docker/cli/cli/context/store"
cliflags "github.com/docker/cli/cli/flags"
manifeststore "github.com/docker/cli/cli/manifest/store"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/cli/trust"
dopts "github.com/docker/cli/opts"
clitypes "github.com/docker/cli/types"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/client"
Expand Down Expand Up @@ -57,6 +56,10 @@ type Cli interface {
RegistryClient(bool) registryclient.RegistryClient
ContentTrustEnabled() bool
NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error)
ContextStore() store.Store
CurrentContext() string
StackOrchestrator(flagValue string) (Orchestrator, error)
DockerEndpoint() docker.Endpoint
}

// DockerCli is an instance the docker command line client.
Expand All @@ -71,8 +74,17 @@ type DockerCli struct {
clientInfo ClientInfo
contentTrust bool
newContainerizeClient func(string) (clitypes.ContainerizedClient, error)
contextStore store.Store
currentContext string
dockerEndpoint docker.Endpoint
}

var storeConfig = store.NewConfig(
func() interface{} { return &DockerContext{} },
store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }),
store.EndpointTypeGetter(kubcontext.KubernetesEndpoint, func() interface{} { return &kubcontext.EndpointMeta{} }),
)

// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
func (cli *DockerCli) DefaultVersion() string {
return cli.clientInfo.DefaultVersion
Expand Down Expand Up @@ -167,14 +179,24 @@ func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.Registry
// line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)

var err error
cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
cli.contextStore = store.New(cliconfig.ContextStoreDir(), storeConfig)
cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore)
if err != nil {
return err
}
endpoint, err := resolveDockerEndpoint(cli.contextStore, cli.currentContext, opts.Common)
if err != nil {
return errors.Wrap(err, "unable to resolve docker endpoint")
}
cli.dockerEndpoint = endpoint

cli.client, err = newAPIClientFromEndpoint(endpoint, cli.configFile)
if tlsconfig.IsErrEncryptedKey(err) {
passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
newClient := func(password string) (client.APIClient, error) {
opts.Common.TLSOptions.Passphrase = password
return NewAPIClientFromFlags(opts.Common, cli.configFile)
endpoint.TLSPassword = password
return newAPIClientFromEndpoint(endpoint, cli.configFile)
}
cli.client, err = getClientWithPassword(passRetriever, newClient)
}
Expand All @@ -198,6 +220,73 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
return nil
}

// NewAPIClientFromFlags creates a new APIClient from command line flags
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
store := store.New(cliconfig.ContextStoreDir(), storeConfig)
contextName, err := resolveContextName(opts, configFile, store)
if err != nil {
return nil, err
}
endpoint, err := resolveDockerEndpoint(store, contextName, opts)
if err != nil {
return nil, errors.Wrap(err, "unable to resolve docker endpoint")
}
return newAPIClientFromEndpoint(endpoint, configFile)
}

func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
clientOpts, err := ep.ClientOpts()
if err != nil {
return nil, err
}
customHeaders := configFile.HTTPHeaders
if customHeaders == nil {
customHeaders = map[string]string{}
}
customHeaders["User-Agent"] = UserAgent()
clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))
return client.NewClientWithOpts(clientOpts...)
}

func resolveDockerEndpoint(s store.Store, contextName string, opts *cliflags.CommonOptions) (docker.Endpoint, error) {
if contextName != "" {
ctxMeta, err := s.GetContextMetadata(contextName)
if err != nil {
return docker.Endpoint{}, err
}
epMeta, err := docker.EndpointFromContext(ctxMeta)
if err != nil {
return docker.Endpoint{}, err
}
return docker.WithTLSData(s, contextName, epMeta)
}
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
if err != nil {
return docker.Endpoint{}, err
}

var (
skipTLSVerify bool
tlsData *dcontext.TLSData
)

if opts.TLSOptions != nil {
skipTLSVerify = opts.TLSOptions.InsecureSkipVerify
tlsData, err = dcontext.TLSDataFromFiles(opts.TLSOptions.CAFile, opts.TLSOptions.CertFile, opts.TLSOptions.KeyFile)
if err != nil {
return docker.Endpoint{}, err
}
}

return docker.Endpoint{
EndpointMeta: docker.EndpointMeta{
Host: host,
SkipTLSVerify: skipTLSVerify,
},
TLSData: tlsData,
}, nil
}

func isEnabled(value string) (bool, error) {
switch value {
case "enabled":
Expand Down Expand Up @@ -253,6 +342,57 @@ func (cli *DockerCli) NewContainerizedEngineClient(sockPath string) (clitypes.Co
return cli.newContainerizeClient(sockPath)
}

// ContextStore returns the ContextStore
func (cli *DockerCli) ContextStore() store.Store {
return cli.contextStore
}

// CurrentContext returns the current context name
func (cli *DockerCli) CurrentContext() string {
return cli.currentContext
}

// StackOrchestrator resolves which stack orchestrator is in use
func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error) {
var ctxOrchestrator string

configFile := cli.configFile
if configFile == nil {
configFile = cliconfig.LoadDefaultConfigFile(cli.Err())
}

currentContext := cli.CurrentContext()
if currentContext == "" {
currentContext = configFile.CurrentContext
}
if currentContext != "" {
contextstore := cli.contextStore
if contextstore == nil {
contextstore = store.New(cliconfig.ContextStoreDir(), storeConfig)
}
ctxRaw, err := contextstore.GetContextMetadata(currentContext)
if store.IsErrContextDoesNotExist(err) {
// case where the currentContext has been removed (CLI behavior is to fallback to using DOCKER_HOST based resolution)
return GetStackOrchestrator(flagValue, "", configFile.StackOrchestrator, cli.Err())
}
if err != nil {
return "", err
}
ctxMeta, err := GetDockerContext(ctxRaw)
if err != nil {
return "", err
}
ctxOrchestrator = string(ctxMeta.StackOrchestrator)
}

return GetStackOrchestrator(flagValue, ctxOrchestrator, configFile.StackOrchestrator, cli.Err())
}

// DockerEndpoint returns the current docker endpoint
func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
return cli.dockerEndpoint
}

// ServerInfo stores details about the supported features and platform of the
// server
type ServerInfo struct {
Expand All @@ -272,51 +412,6 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool, containe
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err, contentTrust: isTrusted, newContainerizeClient: containerizedFn}
}

// NewAPIClientFromFlags creates a new APIClient from command line flags
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
if err != nil {
return &client.Client{}, err
}
var clientOpts []func(*client.Client) error
helper, err := connhelper.GetConnectionHelper(host)
if err != nil {
return &client.Client{}, err
}
if helper == nil {
clientOpts = append(clientOpts, withHTTPClient(opts.TLSOptions))
clientOpts = append(clientOpts, client.WithHost(host))
} else {
clientOpts = append(clientOpts, func(c *client.Client) error {
httpClient := &http.Client{
// No tls
// No proxy
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
return client.WithHTTPClient(httpClient)(c)
})
clientOpts = append(clientOpts, client.WithHost(helper.Host))
clientOpts = append(clientOpts, client.WithDialContext(helper.Dialer))
}

customHeaders := configFile.HTTPHeaders
if customHeaders == nil {
customHeaders = map[string]string{}
}
customHeaders["User-Agent"] = UserAgent()
clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))

verStr := api.DefaultVersion
if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" {
verStr = tmpStr
}
clientOpts = append(clientOpts, client.WithVersion(verStr))

return client.NewClientWithOpts(clientOpts...)
}

func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
var host string
switch len(hosts) {
Expand All @@ -331,35 +426,41 @@ func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error
return dopts.ParseHost(tlsOptions != nil, host)
}

func withHTTPClient(tlsOpts *tlsconfig.Options) func(*client.Client) error {
return func(c *client.Client) error {
if tlsOpts == nil {
// Use the default HTTPClient
return nil
}

opts := *tlsOpts
opts.ExclusiveRootPools = true
tlsConfig, err := tlsconfig.Client(opts)
if err != nil {
return err
}

httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
DialContext: (&net.Dialer{
KeepAlive: 30 * time.Second,
Timeout: 30 * time.Second,
}).DialContext,
},
CheckRedirect: client.CheckRedirect,
}
return client.WithHTTPClient(httpClient)(c)
}
}

// UserAgent returns the user agent string used for making API requests
func UserAgent() string {
return "Docker-Client/" + cli.Version + " (" + runtime.GOOS + ")"
}

// resolveContextName resolves the current context name with the following rules:
// - setting both --context and --host flags is ambiguous
// - if --context is set, use this value
// - if --host flag or DOCKER_HOST is set, fallbacks to use the same logic as before context-store was added
// for backward compatibility with existing scripts
// - if DOCKER_CONTEXT is set, use this value
// - if Config file has a globally set "CurrentContext", use this value
// - fallbacks to default HOST, uses TLS config from flags/env vars
func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigFile, contextstore store.Store) (string, error) {
if opts.Context != "" && len(opts.Hosts) > 0 {
return "", errors.New("Conflicting options: either specify --host or --context, not both")
}
if opts.Context != "" {
return opts.Context, nil
}
if len(opts.Hosts) > 0 {
return "", nil
}
if _, present := os.LookupEnv("DOCKER_HOST"); present {
return "", nil
}
if ctxName, ok := os.LookupEnv("DOCKER_CONTEXT"); ok {
return ctxName, nil
}
if config != nil && config.CurrentContext != "" {
_, err := contextstore.GetContextMetadata(config.CurrentContext)
if store.IsErrContextDoesNotExist(err) {
return "", errors.Errorf("Current context %q is not found on the file system, please check your config file at %s", config.CurrentContext, config.Filename)
}
return config.CurrentContext, err
}
return "", nil
}
1 change: 1 addition & 0 deletions cli/command/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
customVersion := "v3.3.3"
defer env.Patch(t, "DOCKER_API_VERSION", customVersion)()
defer env.Patch(t, "DOCKER_HOST", ":2375")()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: shouldn't this be in another test (subtest ? 🙃)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is required to make sure the initializer goes trough the "don't use context store" code path. (I consider those tests as non-regression tests for existing scripts)


opts := &flags.CommonOptions{}
configFile := &configfile.ConfigFile{}
Expand Down
4 changes: 4 additions & 0 deletions cli/command/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/docker/cli/cli/command/checkpoint"
"github.com/docker/cli/cli/command/config"
"github.com/docker/cli/cli/command/container"
"github.com/docker/cli/cli/command/context"
"github.com/docker/cli/cli/command/engine"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/command/manifest"
Expand Down Expand Up @@ -86,6 +87,9 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
// volume
volume.NewVolumeCommand(dockerCli),

// context
context.NewContextCommand(dockerCli),

// legacy commands may be hidden
hide(system.NewEventsCommand(dockerCli)),
hide(system.NewInfoCommand(dockerCli)),
Expand Down
27 changes: 27 additions & 0 deletions cli/command/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package command

import (
"errors"

"github.com/docker/cli/cli/context/store"
)

// DockerContext is a typed representation of what we put in Context metadata
type DockerContext struct {
Description string `json:",omitempty"`
StackOrchestrator Orchestrator `json:",omitempty"`
}

// GetDockerContext extracts metadata from stored context metadata
func GetDockerContext(storeMetadata store.ContextMetadata) (DockerContext, error) {
if storeMetadata.Metadata == nil {
// can happen if we save endpoints before assigning a context metadata
// it is totally valid, and we should return a default initialized value
return DockerContext{}, nil
}
res, ok := storeMetadata.Metadata.(DockerContext)
if !ok {
return DockerContext{}, errors.New("context metadata is not a valid DockerContext")
}
return res, nil
}
Loading