github.com/ali-iotechsys/cli@v20.10.0+incompatible/cli/command/cli.go (about)

     1  package command
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/docker/cli/cli/config"
    15  	cliconfig "github.com/docker/cli/cli/config"
    16  	"github.com/docker/cli/cli/config/configfile"
    17  	dcontext "github.com/docker/cli/cli/context"
    18  	"github.com/docker/cli/cli/context/docker"
    19  	"github.com/docker/cli/cli/context/store"
    20  	"github.com/docker/cli/cli/debug"
    21  	cliflags "github.com/docker/cli/cli/flags"
    22  	manifeststore "github.com/docker/cli/cli/manifest/store"
    23  	registryclient "github.com/docker/cli/cli/registry/client"
    24  	"github.com/docker/cli/cli/streams"
    25  	"github.com/docker/cli/cli/trust"
    26  	"github.com/docker/cli/cli/version"
    27  	dopts "github.com/docker/cli/opts"
    28  	"github.com/docker/docker/api"
    29  	"github.com/docker/docker/api/types"
    30  	registrytypes "github.com/docker/docker/api/types/registry"
    31  	"github.com/docker/docker/client"
    32  	"github.com/docker/go-connections/tlsconfig"
    33  	"github.com/moby/term"
    34  	"github.com/pkg/errors"
    35  	"github.com/spf13/cobra"
    36  	"github.com/theupdateframework/notary"
    37  	notaryclient "github.com/theupdateframework/notary/client"
    38  	"github.com/theupdateframework/notary/passphrase"
    39  )
    40  
    41  // Streams is an interface which exposes the standard input and output streams
    42  type Streams interface {
    43  	In() *streams.In
    44  	Out() *streams.Out
    45  	Err() io.Writer
    46  }
    47  
    48  // Cli represents the docker command line client.
    49  type Cli interface {
    50  	Client() client.APIClient
    51  	Out() *streams.Out
    52  	Err() io.Writer
    53  	In() *streams.In
    54  	SetIn(in *streams.In)
    55  	Apply(ops ...DockerCliOption) error
    56  	ConfigFile() *configfile.ConfigFile
    57  	ServerInfo() ServerInfo
    58  	ClientInfo() ClientInfo
    59  	NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
    60  	DefaultVersion() string
    61  	ManifestStore() manifeststore.Store
    62  	RegistryClient(bool) registryclient.RegistryClient
    63  	ContentTrustEnabled() bool
    64  	ContextStore() store.Store
    65  	CurrentContext() string
    66  	StackOrchestrator(flagValue string) (Orchestrator, error)
    67  	DockerEndpoint() docker.Endpoint
    68  }
    69  
    70  // DockerCli is an instance the docker command line client.
    71  // Instances of the client can be returned from NewDockerCli.
    72  type DockerCli struct {
    73  	configFile         *configfile.ConfigFile
    74  	in                 *streams.In
    75  	out                *streams.Out
    76  	err                io.Writer
    77  	client             client.APIClient
    78  	serverInfo         ServerInfo
    79  	clientInfo         *ClientInfo
    80  	contentTrust       bool
    81  	contextStore       store.Store
    82  	currentContext     string
    83  	dockerEndpoint     docker.Endpoint
    84  	contextStoreConfig store.Config
    85  }
    86  
    87  // DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
    88  func (cli *DockerCli) DefaultVersion() string {
    89  	return cli.ClientInfo().DefaultVersion
    90  }
    91  
    92  // Client returns the APIClient
    93  func (cli *DockerCli) Client() client.APIClient {
    94  	return cli.client
    95  }
    96  
    97  // Out returns the writer used for stdout
    98  func (cli *DockerCli) Out() *streams.Out {
    99  	return cli.out
   100  }
   101  
   102  // Err returns the writer used for stderr
   103  func (cli *DockerCli) Err() io.Writer {
   104  	return cli.err
   105  }
   106  
   107  // SetIn sets the reader used for stdin
   108  func (cli *DockerCli) SetIn(in *streams.In) {
   109  	cli.in = in
   110  }
   111  
   112  // In returns the reader used for stdin
   113  func (cli *DockerCli) In() *streams.In {
   114  	return cli.in
   115  }
   116  
   117  // ShowHelp shows the command help.
   118  func ShowHelp(err io.Writer) func(*cobra.Command, []string) error {
   119  	return func(cmd *cobra.Command, args []string) error {
   120  		cmd.SetOut(err)
   121  		cmd.HelpFunc()(cmd, args)
   122  		return nil
   123  	}
   124  }
   125  
   126  // ConfigFile returns the ConfigFile
   127  func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
   128  	if cli.configFile == nil {
   129  		cli.loadConfigFile()
   130  	}
   131  	return cli.configFile
   132  }
   133  
   134  func (cli *DockerCli) loadConfigFile() {
   135  	cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
   136  }
   137  
   138  // ServerInfo returns the server version details for the host this client is
   139  // connected to
   140  func (cli *DockerCli) ServerInfo() ServerInfo {
   141  	return cli.serverInfo
   142  }
   143  
   144  // ClientInfo returns the client details for the cli
   145  func (cli *DockerCli) ClientInfo() ClientInfo {
   146  	if cli.clientInfo == nil {
   147  		if err := cli.loadClientInfo(); err != nil {
   148  			panic(err)
   149  		}
   150  	}
   151  	return *cli.clientInfo
   152  }
   153  
   154  func (cli *DockerCli) loadClientInfo() error {
   155  	var v string
   156  	if cli.client != nil {
   157  		v = cli.client.ClientVersion()
   158  	} else {
   159  		v = api.DefaultVersion
   160  	}
   161  	cli.clientInfo = &ClientInfo{
   162  		DefaultVersion:  v,
   163  		HasExperimental: true,
   164  	}
   165  	return nil
   166  }
   167  
   168  // ContentTrustEnabled returns whether content trust has been enabled by an
   169  // environment variable.
   170  func (cli *DockerCli) ContentTrustEnabled() bool {
   171  	return cli.contentTrust
   172  }
   173  
   174  // BuildKitEnabled returns whether buildkit is enabled either through a daemon setting
   175  // or otherwise the client-side DOCKER_BUILDKIT environment variable
   176  func BuildKitEnabled(si ServerInfo) (bool, error) {
   177  	buildkitEnabled := si.BuildkitVersion == types.BuilderBuildKit
   178  	if buildkitEnv := os.Getenv("DOCKER_BUILDKIT"); buildkitEnv != "" {
   179  		var err error
   180  		buildkitEnabled, err = strconv.ParseBool(buildkitEnv)
   181  		if err != nil {
   182  			return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
   183  		}
   184  	}
   185  	return buildkitEnabled, nil
   186  }
   187  
   188  // ManifestStore returns a store for local manifests
   189  func (cli *DockerCli) ManifestStore() manifeststore.Store {
   190  	// TODO: support override default location from config file
   191  	return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
   192  }
   193  
   194  // RegistryClient returns a client for communicating with a Docker distribution
   195  // registry
   196  func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
   197  	resolver := func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
   198  		return ResolveAuthConfig(ctx, cli, index)
   199  	}
   200  	return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
   201  }
   202  
   203  // InitializeOpt is the type of the functional options passed to DockerCli.Initialize
   204  type InitializeOpt func(dockerCli *DockerCli) error
   205  
   206  // WithInitializeClient is passed to DockerCli.Initialize by callers who wish to set a particular API Client for use by the CLI.
   207  func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) InitializeOpt {
   208  	return func(dockerCli *DockerCli) error {
   209  		var err error
   210  		dockerCli.client, err = makeClient(dockerCli)
   211  		return err
   212  	}
   213  }
   214  
   215  // Initialize the dockerCli runs initialization that must happen after command
   216  // line flags are parsed.
   217  func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...InitializeOpt) error {
   218  	var err error
   219  
   220  	for _, o := range ops {
   221  		if err := o(cli); err != nil {
   222  			return err
   223  		}
   224  	}
   225  	cliflags.SetLogLevel(opts.Common.LogLevel)
   226  
   227  	if opts.ConfigDir != "" {
   228  		cliconfig.SetDir(opts.ConfigDir)
   229  	}
   230  
   231  	if opts.Common.Debug {
   232  		debug.Enable()
   233  	}
   234  
   235  	cli.loadConfigFile()
   236  
   237  	baseContextStore := store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig)
   238  	cli.contextStore = &ContextStoreWithDefault{
   239  		Store: baseContextStore,
   240  		Resolver: func() (*DefaultContext, error) {
   241  			return ResolveDefaultContext(opts.Common, cli.ConfigFile(), cli.contextStoreConfig, cli.Err())
   242  		},
   243  	}
   244  	cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore)
   245  	if err != nil {
   246  		return err
   247  	}
   248  	cli.dockerEndpoint, err = resolveDockerEndpoint(cli.contextStore, cli.currentContext)
   249  	if err != nil {
   250  		return errors.Wrap(err, "unable to resolve docker endpoint")
   251  	}
   252  
   253  	if cli.client == nil {
   254  		cli.client, err = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile)
   255  		if tlsconfig.IsErrEncryptedKey(err) {
   256  			passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
   257  			newClient := func(password string) (client.APIClient, error) {
   258  				cli.dockerEndpoint.TLSPassword = password
   259  				return newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile)
   260  			}
   261  			cli.client, err = getClientWithPassword(passRetriever, newClient)
   262  		}
   263  		if err != nil {
   264  			return err
   265  		}
   266  	}
   267  	cli.initializeFromClient()
   268  
   269  	if err := cli.loadClientInfo(); err != nil {
   270  		return err
   271  	}
   272  
   273  	return nil
   274  }
   275  
   276  // NewAPIClientFromFlags creates a new APIClient from command line flags
   277  func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
   278  	storeConfig := DefaultContextStoreConfig()
   279  	store := &ContextStoreWithDefault{
   280  		Store: store.New(cliconfig.ContextStoreDir(), storeConfig),
   281  		Resolver: func() (*DefaultContext, error) {
   282  			return ResolveDefaultContext(opts, configFile, storeConfig, ioutil.Discard)
   283  		},
   284  	}
   285  	contextName, err := resolveContextName(opts, configFile, store)
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  	endpoint, err := resolveDockerEndpoint(store, contextName)
   290  	if err != nil {
   291  		return nil, errors.Wrap(err, "unable to resolve docker endpoint")
   292  	}
   293  	return newAPIClientFromEndpoint(endpoint, configFile)
   294  }
   295  
   296  func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
   297  	clientOpts, err := ep.ClientOpts()
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  	customHeaders := make(map[string]string, len(configFile.HTTPHeaders))
   302  	for k, v := range configFile.HTTPHeaders {
   303  		customHeaders[k] = v
   304  	}
   305  	customHeaders["User-Agent"] = UserAgent()
   306  	clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))
   307  	return client.NewClientWithOpts(clientOpts...)
   308  }
   309  
   310  func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) {
   311  	ctxMeta, err := s.GetMetadata(contextName)
   312  	if err != nil {
   313  		return docker.Endpoint{}, err
   314  	}
   315  	epMeta, err := docker.EndpointFromContext(ctxMeta)
   316  	if err != nil {
   317  		return docker.Endpoint{}, err
   318  	}
   319  	return docker.WithTLSData(s, contextName, epMeta)
   320  }
   321  
   322  // Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
   323  func resolveDefaultDockerEndpoint(opts *cliflags.CommonOptions) (docker.Endpoint, error) {
   324  	host, err := getServerHost(opts.Hosts, opts.TLSOptions)
   325  	if err != nil {
   326  		return docker.Endpoint{}, err
   327  	}
   328  
   329  	var (
   330  		skipTLSVerify bool
   331  		tlsData       *dcontext.TLSData
   332  	)
   333  
   334  	if opts.TLSOptions != nil {
   335  		skipTLSVerify = opts.TLSOptions.InsecureSkipVerify
   336  		tlsData, err = dcontext.TLSDataFromFiles(opts.TLSOptions.CAFile, opts.TLSOptions.CertFile, opts.TLSOptions.KeyFile)
   337  		if err != nil {
   338  			return docker.Endpoint{}, err
   339  		}
   340  	}
   341  
   342  	return docker.Endpoint{
   343  		EndpointMeta: docker.EndpointMeta{
   344  			Host:          host,
   345  			SkipTLSVerify: skipTLSVerify,
   346  		},
   347  		TLSData: tlsData,
   348  	}, nil
   349  }
   350  
   351  func (cli *DockerCli) initializeFromClient() {
   352  	ctx := context.Background()
   353  	if strings.HasPrefix(cli.DockerEndpoint().Host, "tcp://") {
   354  		// @FIXME context.WithTimeout doesn't work with connhelper / ssh connections
   355  		// time="2020-04-10T10:16:26Z" level=warning msg="commandConn.CloseWrite: commandconn: failed to wait: signal: killed"
   356  		var cancel func()
   357  		ctx, cancel = context.WithTimeout(ctx, 2*time.Second)
   358  		defer cancel()
   359  	}
   360  
   361  	ping, err := cli.client.Ping(ctx)
   362  	if err != nil {
   363  		// Default to true if we fail to connect to daemon
   364  		cli.serverInfo = ServerInfo{HasExperimental: true}
   365  
   366  		if ping.APIVersion != "" {
   367  			cli.client.NegotiateAPIVersionPing(ping)
   368  		}
   369  		return
   370  	}
   371  
   372  	cli.serverInfo = ServerInfo{
   373  		HasExperimental: ping.Experimental,
   374  		OSType:          ping.OSType,
   375  		BuildkitVersion: ping.BuilderVersion,
   376  	}
   377  	cli.client.NegotiateAPIVersionPing(ping)
   378  }
   379  
   380  func getClientWithPassword(passRetriever notary.PassRetriever, newClient func(password string) (client.APIClient, error)) (client.APIClient, error) {
   381  	for attempts := 0; ; attempts++ {
   382  		passwd, giveup, err := passRetriever("private", "encrypted TLS private", false, attempts)
   383  		if giveup || err != nil {
   384  			return nil, errors.Wrap(err, "private key is encrypted, but could not get passphrase")
   385  		}
   386  
   387  		apiclient, err := newClient(passwd)
   388  		if !tlsconfig.IsErrEncryptedKey(err) {
   389  			return apiclient, err
   390  		}
   391  	}
   392  }
   393  
   394  // NotaryClient provides a Notary Repository to interact with signed metadata for an image
   395  func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
   396  	return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
   397  }
   398  
   399  // ContextStore returns the ContextStore
   400  func (cli *DockerCli) ContextStore() store.Store {
   401  	return cli.contextStore
   402  }
   403  
   404  // CurrentContext returns the current context name
   405  func (cli *DockerCli) CurrentContext() string {
   406  	return cli.currentContext
   407  }
   408  
   409  // StackOrchestrator resolves which stack orchestrator is in use
   410  func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error) {
   411  	currentContext := cli.CurrentContext()
   412  	ctxRaw, err := cli.ContextStore().GetMetadata(currentContext)
   413  	if store.IsErrContextDoesNotExist(err) {
   414  		// case where the currentContext has been removed (CLI behavior is to fallback to using DOCKER_HOST based resolution)
   415  		return GetStackOrchestrator(flagValue, "", cli.ConfigFile().StackOrchestrator, cli.Err())
   416  	}
   417  	if err != nil {
   418  		return "", err
   419  	}
   420  	ctxMeta, err := GetDockerContext(ctxRaw)
   421  	if err != nil {
   422  		return "", err
   423  	}
   424  	ctxOrchestrator := string(ctxMeta.StackOrchestrator)
   425  	return GetStackOrchestrator(flagValue, ctxOrchestrator, cli.ConfigFile().StackOrchestrator, cli.Err())
   426  }
   427  
   428  // DockerEndpoint returns the current docker endpoint
   429  func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
   430  	return cli.dockerEndpoint
   431  }
   432  
   433  // Apply all the operation on the cli
   434  func (cli *DockerCli) Apply(ops ...DockerCliOption) error {
   435  	for _, op := range ops {
   436  		if err := op(cli); err != nil {
   437  			return err
   438  		}
   439  	}
   440  	return nil
   441  }
   442  
   443  // ServerInfo stores details about the supported features and platform of the
   444  // server
   445  type ServerInfo struct {
   446  	HasExperimental bool
   447  	OSType          string
   448  	BuildkitVersion types.BuilderVersion
   449  }
   450  
   451  // ClientInfo stores details about the supported features of the client
   452  type ClientInfo struct {
   453  	// Deprecated: experimental CLI features always enabled. This field is kept
   454  	// for backward-compatibility, and is always "true".
   455  	HasExperimental bool
   456  	DefaultVersion  string
   457  }
   458  
   459  // NewDockerCli returns a DockerCli instance with all operators applied on it.
   460  // It applies by default the standard streams, and the content trust from
   461  // environment.
   462  func NewDockerCli(ops ...DockerCliOption) (*DockerCli, error) {
   463  	cli := &DockerCli{}
   464  	defaultOps := []DockerCliOption{
   465  		WithContentTrustFromEnv(),
   466  	}
   467  	cli.contextStoreConfig = DefaultContextStoreConfig()
   468  	ops = append(defaultOps, ops...)
   469  	if err := cli.Apply(ops...); err != nil {
   470  		return nil, err
   471  	}
   472  	if cli.out == nil || cli.in == nil || cli.err == nil {
   473  		stdin, stdout, stderr := term.StdStreams()
   474  		if cli.in == nil {
   475  			cli.in = streams.NewIn(stdin)
   476  		}
   477  		if cli.out == nil {
   478  			cli.out = streams.NewOut(stdout)
   479  		}
   480  		if cli.err == nil {
   481  			cli.err = stderr
   482  		}
   483  	}
   484  	return cli, nil
   485  }
   486  
   487  func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
   488  	var host string
   489  	switch len(hosts) {
   490  	case 0:
   491  		host = os.Getenv("DOCKER_HOST")
   492  	case 1:
   493  		host = hosts[0]
   494  	default:
   495  		return "", errors.New("Please specify only one -H")
   496  	}
   497  
   498  	return dopts.ParseHost(tlsOptions != nil, host)
   499  }
   500  
   501  // UserAgent returns the user agent string used for making API requests
   502  func UserAgent() string {
   503  	return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
   504  }
   505  
   506  // resolveContextName resolves the current context name with the following rules:
   507  // - setting both --context and --host flags is ambiguous
   508  // - if --context is set, use this value
   509  // - if --host flag or DOCKER_HOST is set, fallbacks to use the same logic as before context-store was added
   510  // for backward compatibility with existing scripts
   511  // - if DOCKER_CONTEXT is set, use this value
   512  // - if Config file has a globally set "CurrentContext", use this value
   513  // - fallbacks to default HOST, uses TLS config from flags/env vars
   514  func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigFile, contextstore store.Reader) (string, error) {
   515  	if opts.Context != "" && len(opts.Hosts) > 0 {
   516  		return "", errors.New("Conflicting options: either specify --host or --context, not both")
   517  	}
   518  	if opts.Context != "" {
   519  		return opts.Context, nil
   520  	}
   521  	if len(opts.Hosts) > 0 {
   522  		return DefaultContextName, nil
   523  	}
   524  	if _, present := os.LookupEnv("DOCKER_HOST"); present {
   525  		return DefaultContextName, nil
   526  	}
   527  	if ctxName, ok := os.LookupEnv("DOCKER_CONTEXT"); ok {
   528  		return ctxName, nil
   529  	}
   530  	if config != nil && config.CurrentContext != "" {
   531  		_, err := contextstore.GetMetadata(config.CurrentContext)
   532  		if store.IsErrContextDoesNotExist(err) {
   533  			return "", errors.Errorf("Current context %q is not found on the file system, please check your config file at %s", config.CurrentContext, config.Filename)
   534  		}
   535  		return config.CurrentContext, err
   536  	}
   537  	return DefaultContextName, nil
   538  }
   539  
   540  var defaultStoreEndpoints = []store.NamedTypeGetter{
   541  	store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }),
   542  }
   543  
   544  // RegisterDefaultStoreEndpoints registers a new named endpoint
   545  // metadata type with the default context store config, so that
   546  // endpoint will be supported by stores using the config returned by
   547  // DefaultContextStoreConfig.
   548  func RegisterDefaultStoreEndpoints(ep ...store.NamedTypeGetter) {
   549  	defaultStoreEndpoints = append(defaultStoreEndpoints, ep...)
   550  }
   551  
   552  // DefaultContextStoreConfig returns a new store.Config with the default set of endpoints configured.
   553  func DefaultContextStoreConfig() store.Config {
   554  	return store.NewConfig(
   555  		func() interface{} { return &DockerContext{} },
   556  		defaultStoreEndpoints...,
   557  	)
   558  }