github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/cli.go (about)

     1  // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
     2  //go:build go1.19
     3  
     4  package command
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strconv"
    14  	"sync"
    15  	"time"
    16  
    17  	"github.com/docker/cli/cli/config"
    18  	"github.com/docker/cli/cli/config/configfile"
    19  	dcontext "github.com/docker/cli/cli/context"
    20  	"github.com/docker/cli/cli/context/docker"
    21  	"github.com/docker/cli/cli/context/store"
    22  	"github.com/docker/cli/cli/debug"
    23  	cliflags "github.com/docker/cli/cli/flags"
    24  	manifeststore "github.com/docker/cli/cli/manifest/store"
    25  	registryclient "github.com/docker/cli/cli/registry/client"
    26  	"github.com/docker/cli/cli/streams"
    27  	"github.com/docker/cli/cli/trust"
    28  	"github.com/docker/cli/cli/version"
    29  	dopts "github.com/docker/cli/opts"
    30  	"github.com/docker/docker/api"
    31  	"github.com/docker/docker/api/types"
    32  	"github.com/docker/docker/api/types/registry"
    33  	"github.com/docker/docker/api/types/swarm"
    34  	"github.com/docker/docker/client"
    35  	"github.com/docker/go-connections/tlsconfig"
    36  	"github.com/pkg/errors"
    37  	"github.com/spf13/cobra"
    38  	notaryclient "github.com/theupdateframework/notary/client"
    39  )
    40  
    41  const defaultInitTimeout = 2 * time.Second
    42  
    43  // Streams is an interface which exposes the standard input and output streams
    44  type Streams interface {
    45  	In() *streams.In
    46  	Out() *streams.Out
    47  	Err() io.Writer
    48  }
    49  
    50  // Cli represents the docker command line client.
    51  type Cli interface {
    52  	Client() client.APIClient
    53  	Streams
    54  	SetIn(in *streams.In)
    55  	Apply(ops ...CLIOption) error
    56  	ConfigFile() *configfile.ConfigFile
    57  	ServerInfo() ServerInfo
    58  	NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
    59  	DefaultVersion() string
    60  	CurrentVersion() string
    61  	ManifestStore() manifeststore.Store
    62  	RegistryClient(bool) registryclient.RegistryClient
    63  	ContentTrustEnabled() bool
    64  	BuildKitEnabled() (bool, error)
    65  	ContextStore() store.Store
    66  	CurrentContext() string
    67  	DockerEndpoint() docker.Endpoint
    68  	TelemetryClient
    69  }
    70  
    71  // DockerCli is an instance the docker command line client.
    72  // Instances of the client can be returned from NewDockerCli.
    73  type DockerCli struct {
    74  	configFile         *configfile.ConfigFile
    75  	options            *cliflags.ClientOptions
    76  	in                 *streams.In
    77  	out                *streams.Out
    78  	err                io.Writer
    79  	client             client.APIClient
    80  	serverInfo         ServerInfo
    81  	contentTrust       bool
    82  	contextStore       store.Store
    83  	currentContext     string
    84  	init               sync.Once
    85  	initErr            error
    86  	dockerEndpoint     docker.Endpoint
    87  	contextStoreConfig store.Config
    88  	initTimeout        time.Duration
    89  	res                telemetryResource
    90  
    91  	// baseCtx is the base context used for internal operations. In the future
    92  	// this may be replaced by explicitly passing a context to functions that
    93  	// need it.
    94  	baseCtx context.Context
    95  }
    96  
    97  // DefaultVersion returns api.defaultVersion.
    98  func (cli *DockerCli) DefaultVersion() string {
    99  	return api.DefaultVersion
   100  }
   101  
   102  // CurrentVersion returns the API version currently negotiated, or the default
   103  // version otherwise.
   104  func (cli *DockerCli) CurrentVersion() string {
   105  	_ = cli.initialize()
   106  	if cli.client == nil {
   107  		return api.DefaultVersion
   108  	}
   109  	return cli.client.ClientVersion()
   110  }
   111  
   112  // Client returns the APIClient
   113  func (cli *DockerCli) Client() client.APIClient {
   114  	if err := cli.initialize(); err != nil {
   115  		_, _ = fmt.Fprintf(cli.Err(), "Failed to initialize: %s\n", err)
   116  		os.Exit(1)
   117  	}
   118  	return cli.client
   119  }
   120  
   121  // Out returns the writer used for stdout
   122  func (cli *DockerCli) Out() *streams.Out {
   123  	return cli.out
   124  }
   125  
   126  // Err returns the writer used for stderr
   127  func (cli *DockerCli) Err() io.Writer {
   128  	return cli.err
   129  }
   130  
   131  // SetIn sets the reader used for stdin
   132  func (cli *DockerCli) SetIn(in *streams.In) {
   133  	cli.in = in
   134  }
   135  
   136  // In returns the reader used for stdin
   137  func (cli *DockerCli) In() *streams.In {
   138  	return cli.in
   139  }
   140  
   141  // ShowHelp shows the command help.
   142  func ShowHelp(err io.Writer) func(*cobra.Command, []string) error {
   143  	return func(cmd *cobra.Command, args []string) error {
   144  		cmd.SetOut(err)
   145  		cmd.HelpFunc()(cmd, args)
   146  		return nil
   147  	}
   148  }
   149  
   150  // ConfigFile returns the ConfigFile
   151  func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
   152  	// TODO(thaJeztah): when would this happen? Is this only in tests (where cli.Initialize() is not called first?)
   153  	if cli.configFile == nil {
   154  		cli.configFile = config.LoadDefaultConfigFile(cli.err)
   155  	}
   156  	return cli.configFile
   157  }
   158  
   159  // ServerInfo returns the server version details for the host this client is
   160  // connected to
   161  func (cli *DockerCli) ServerInfo() ServerInfo {
   162  	_ = cli.initialize()
   163  	return cli.serverInfo
   164  }
   165  
   166  // ContentTrustEnabled returns whether content trust has been enabled by an
   167  // environment variable.
   168  func (cli *DockerCli) ContentTrustEnabled() bool {
   169  	return cli.contentTrust
   170  }
   171  
   172  // BuildKitEnabled returns buildkit is enabled or not.
   173  func (cli *DockerCli) BuildKitEnabled() (bool, error) {
   174  	// use DOCKER_BUILDKIT env var value if set and not empty
   175  	if v := os.Getenv("DOCKER_BUILDKIT"); v != "" {
   176  		enabled, err := strconv.ParseBool(v)
   177  		if err != nil {
   178  			return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
   179  		}
   180  		return enabled, nil
   181  	}
   182  	// if a builder alias is defined, we are using BuildKit
   183  	aliasMap := cli.ConfigFile().Aliases
   184  	if _, ok := aliasMap["builder"]; ok {
   185  		return true, nil
   186  	}
   187  	// otherwise, assume BuildKit is enabled but
   188  	// not if wcow reported from server side
   189  	return cli.ServerInfo().OSType != "windows", nil
   190  }
   191  
   192  // HooksEnabled returns whether plugin hooks are enabled.
   193  func (cli *DockerCli) HooksEnabled() bool {
   194  	// legacy support DOCKER_CLI_HINTS env var
   195  	if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
   196  		enabled, err := strconv.ParseBool(v)
   197  		if err != nil {
   198  			return false
   199  		}
   200  		return enabled
   201  	}
   202  	// use DOCKER_CLI_HOOKS env var value if set and not empty
   203  	if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
   204  		enabled, err := strconv.ParseBool(v)
   205  		if err != nil {
   206  			return false
   207  		}
   208  		return enabled
   209  	}
   210  	featuresMap := cli.ConfigFile().Features
   211  	if v, ok := featuresMap["hooks"]; ok {
   212  		enabled, err := strconv.ParseBool(v)
   213  		if err != nil {
   214  			return false
   215  		}
   216  		return enabled
   217  	}
   218  	// default to false
   219  	return false
   220  }
   221  
   222  // ManifestStore returns a store for local manifests
   223  func (cli *DockerCli) ManifestStore() manifeststore.Store {
   224  	// TODO: support override default location from config file
   225  	return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
   226  }
   227  
   228  // RegistryClient returns a client for communicating with a Docker distribution
   229  // registry
   230  func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
   231  	resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig {
   232  		return ResolveAuthConfig(cli.ConfigFile(), index)
   233  	}
   234  	return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
   235  }
   236  
   237  // WithInitializeClient is passed to DockerCli.Initialize by callers who wish to set a particular API Client for use by the CLI.
   238  func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) CLIOption {
   239  	return func(dockerCli *DockerCli) error {
   240  		var err error
   241  		dockerCli.client, err = makeClient(dockerCli)
   242  		return err
   243  	}
   244  }
   245  
   246  // Initialize the dockerCli runs initialization that must happen after command
   247  // line flags are parsed.
   248  func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error {
   249  	for _, o := range ops {
   250  		if err := o(cli); err != nil {
   251  			return err
   252  		}
   253  	}
   254  	cliflags.SetLogLevel(opts.LogLevel)
   255  
   256  	if opts.ConfigDir != "" {
   257  		config.SetDir(opts.ConfigDir)
   258  	}
   259  
   260  	if opts.Debug {
   261  		debug.Enable()
   262  	}
   263  	if opts.Context != "" && len(opts.Hosts) > 0 {
   264  		return errors.New("conflicting options: either specify --host or --context, not both")
   265  	}
   266  
   267  	cli.options = opts
   268  	cli.configFile = config.LoadDefaultConfigFile(cli.err)
   269  	cli.currentContext = resolveContextName(cli.options, cli.configFile)
   270  	cli.contextStore = &ContextStoreWithDefault{
   271  		Store: store.New(config.ContextStoreDir(), cli.contextStoreConfig),
   272  		Resolver: func() (*DefaultContext, error) {
   273  			return ResolveDefaultContext(cli.options, cli.contextStoreConfig)
   274  		},
   275  	}
   276  
   277  	// TODO(krissetto): pass ctx to the funcs instead of using this
   278  	cli.createGlobalMeterProvider(cli.baseCtx)
   279  	cli.createGlobalTracerProvider(cli.baseCtx)
   280  
   281  	return nil
   282  }
   283  
   284  // NewAPIClientFromFlags creates a new APIClient from command line flags
   285  func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
   286  	if opts.Context != "" && len(opts.Hosts) > 0 {
   287  		return nil, errors.New("conflicting options: either specify --host or --context, not both")
   288  	}
   289  
   290  	storeConfig := DefaultContextStoreConfig()
   291  	contextStore := &ContextStoreWithDefault{
   292  		Store: store.New(config.ContextStoreDir(), storeConfig),
   293  		Resolver: func() (*DefaultContext, error) {
   294  			return ResolveDefaultContext(opts, storeConfig)
   295  		},
   296  	}
   297  	endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile))
   298  	if err != nil {
   299  		return nil, errors.Wrap(err, "unable to resolve docker endpoint")
   300  	}
   301  	return newAPIClientFromEndpoint(endpoint, configFile)
   302  }
   303  
   304  func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
   305  	opts, err := ep.ClientOpts()
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  	if len(configFile.HTTPHeaders) > 0 {
   310  		opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders))
   311  	}
   312  	opts = append(opts, client.WithUserAgent(UserAgent()))
   313  	return client.NewClientWithOpts(opts...)
   314  }
   315  
   316  func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) {
   317  	if s == nil {
   318  		return docker.Endpoint{}, fmt.Errorf("no context store initialized")
   319  	}
   320  	ctxMeta, err := s.GetMetadata(contextName)
   321  	if err != nil {
   322  		return docker.Endpoint{}, err
   323  	}
   324  	epMeta, err := docker.EndpointFromContext(ctxMeta)
   325  	if err != nil {
   326  		return docker.Endpoint{}, err
   327  	}
   328  	return docker.WithTLSData(s, contextName, epMeta)
   329  }
   330  
   331  // Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
   332  func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) {
   333  	host, err := getServerHost(opts.Hosts, opts.TLSOptions)
   334  	if err != nil {
   335  		return docker.Endpoint{}, err
   336  	}
   337  
   338  	var (
   339  		skipTLSVerify bool
   340  		tlsData       *dcontext.TLSData
   341  	)
   342  
   343  	if opts.TLSOptions != nil {
   344  		skipTLSVerify = opts.TLSOptions.InsecureSkipVerify
   345  		tlsData, err = dcontext.TLSDataFromFiles(opts.TLSOptions.CAFile, opts.TLSOptions.CertFile, opts.TLSOptions.KeyFile)
   346  		if err != nil {
   347  			return docker.Endpoint{}, err
   348  		}
   349  	}
   350  
   351  	return docker.Endpoint{
   352  		EndpointMeta: docker.EndpointMeta{
   353  			Host:          host,
   354  			SkipTLSVerify: skipTLSVerify,
   355  		},
   356  		TLSData: tlsData,
   357  	}, nil
   358  }
   359  
   360  func (cli *DockerCli) getInitTimeout() time.Duration {
   361  	if cli.initTimeout != 0 {
   362  		return cli.initTimeout
   363  	}
   364  	return defaultInitTimeout
   365  }
   366  
   367  func (cli *DockerCli) initializeFromClient() {
   368  	ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout())
   369  	defer cancel()
   370  
   371  	ping, err := cli.client.Ping(ctx)
   372  	if err != nil {
   373  		// Default to true if we fail to connect to daemon
   374  		cli.serverInfo = ServerInfo{HasExperimental: true}
   375  
   376  		if ping.APIVersion != "" {
   377  			cli.client.NegotiateAPIVersionPing(ping)
   378  		}
   379  		return
   380  	}
   381  
   382  	cli.serverInfo = ServerInfo{
   383  		HasExperimental: ping.Experimental,
   384  		OSType:          ping.OSType,
   385  		BuildkitVersion: ping.BuilderVersion,
   386  		SwarmStatus:     ping.SwarmStatus,
   387  	}
   388  	cli.client.NegotiateAPIVersionPing(ping)
   389  }
   390  
   391  // NotaryClient provides a Notary Repository to interact with signed metadata for an image
   392  func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
   393  	return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
   394  }
   395  
   396  // ContextStore returns the ContextStore
   397  func (cli *DockerCli) ContextStore() store.Store {
   398  	return cli.contextStore
   399  }
   400  
   401  // CurrentContext returns the current context name, based on flags,
   402  // environment variables and the cli configuration file, in the following
   403  // order of preference:
   404  //
   405  //  1. The "--context" command-line option.
   406  //  2. The "DOCKER_CONTEXT" environment variable ([EnvOverrideContext]).
   407  //  3. The current context as configured through the in "currentContext"
   408  //     field in the CLI configuration file ("~/.docker/config.json").
   409  //  4. If no context is configured, use the "default" context.
   410  //
   411  // # Fallbacks for backward-compatibility
   412  //
   413  // To preserve backward-compatibility with the "pre-contexts" behavior,
   414  // the "default" context is used if:
   415  //
   416  //   - The "--host" option is set
   417  //   - The "DOCKER_HOST" ([client.EnvOverrideHost]) environment variable is set
   418  //     to a non-empty value.
   419  //
   420  // In these cases, the default context is used, which uses the host as
   421  // specified in "DOCKER_HOST", and TLS config from flags/env vars.
   422  //
   423  // Setting both the "--context" and "--host" flags is ambiguous and results
   424  // in an error when the cli is started.
   425  //
   426  // CurrentContext does not validate if the given context exists or if it's
   427  // valid; errors may occur when trying to use it.
   428  func (cli *DockerCli) CurrentContext() string {
   429  	return cli.currentContext
   430  }
   431  
   432  // CurrentContext returns the current context name, based on flags,
   433  // environment variables and the cli configuration file. It does not
   434  // validate if the given context exists or if it's valid; errors may
   435  // occur when trying to use it.
   436  //
   437  // Refer to [DockerCli.CurrentContext] above for further details.
   438  func resolveContextName(opts *cliflags.ClientOptions, cfg *configfile.ConfigFile) string {
   439  	if opts != nil && opts.Context != "" {
   440  		return opts.Context
   441  	}
   442  	if opts != nil && len(opts.Hosts) > 0 {
   443  		return DefaultContextName
   444  	}
   445  	if os.Getenv(client.EnvOverrideHost) != "" {
   446  		return DefaultContextName
   447  	}
   448  	if ctxName := os.Getenv(EnvOverrideContext); ctxName != "" {
   449  		return ctxName
   450  	}
   451  	if cfg != nil && cfg.CurrentContext != "" {
   452  		// We don't validate if this context exists: errors may occur when trying to use it.
   453  		return cfg.CurrentContext
   454  	}
   455  	return DefaultContextName
   456  }
   457  
   458  // DockerEndpoint returns the current docker endpoint
   459  func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
   460  	if err := cli.initialize(); err != nil {
   461  		// Note that we're not terminating here, as this function may be used
   462  		// in cases where we're able to continue.
   463  		_, _ = fmt.Fprintf(cli.Err(), "%v\n", cli.initErr)
   464  	}
   465  	return cli.dockerEndpoint
   466  }
   467  
   468  func (cli *DockerCli) getDockerEndPoint() (ep docker.Endpoint, err error) {
   469  	cn := cli.CurrentContext()
   470  	if cn == DefaultContextName {
   471  		return resolveDefaultDockerEndpoint(cli.options)
   472  	}
   473  	return resolveDockerEndpoint(cli.contextStore, cn)
   474  }
   475  
   476  func (cli *DockerCli) initialize() error {
   477  	cli.init.Do(func() {
   478  		cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint()
   479  		if cli.initErr != nil {
   480  			cli.initErr = errors.Wrap(cli.initErr, "unable to resolve docker endpoint")
   481  			return
   482  		}
   483  		if cli.client == nil {
   484  			if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil {
   485  				return
   486  			}
   487  		}
   488  		if cli.baseCtx == nil {
   489  			cli.baseCtx = context.Background()
   490  		}
   491  		cli.initializeFromClient()
   492  	})
   493  	return cli.initErr
   494  }
   495  
   496  // Apply all the operation on the cli
   497  func (cli *DockerCli) Apply(ops ...CLIOption) error {
   498  	for _, op := range ops {
   499  		if err := op(cli); err != nil {
   500  			return err
   501  		}
   502  	}
   503  	return nil
   504  }
   505  
   506  // ServerInfo stores details about the supported features and platform of the
   507  // server
   508  type ServerInfo struct {
   509  	HasExperimental bool
   510  	OSType          string
   511  	BuildkitVersion types.BuilderVersion
   512  
   513  	// SwarmStatus provides information about the current swarm status of the
   514  	// engine, obtained from the "Swarm" header in the API response.
   515  	//
   516  	// It can be a nil struct if the API version does not provide this header
   517  	// in the ping response, or if an error occurred, in which case the client
   518  	// should use other ways to get the current swarm status, such as the /swarm
   519  	// endpoint.
   520  	SwarmStatus *swarm.Status
   521  }
   522  
   523  // NewDockerCli returns a DockerCli instance with all operators applied on it.
   524  // It applies by default the standard streams, and the content trust from
   525  // environment.
   526  func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
   527  	defaultOps := []CLIOption{
   528  		WithContentTrustFromEnv(),
   529  		WithDefaultContextStoreConfig(),
   530  		WithStandardStreams(),
   531  	}
   532  	ops = append(defaultOps, ops...)
   533  
   534  	cli := &DockerCli{baseCtx: context.Background()}
   535  	if err := cli.Apply(ops...); err != nil {
   536  		return nil, err
   537  	}
   538  	return cli, nil
   539  }
   540  
   541  func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
   542  	var host string
   543  	switch len(hosts) {
   544  	case 0:
   545  		host = os.Getenv(client.EnvOverrideHost)
   546  	case 1:
   547  		host = hosts[0]
   548  	default:
   549  		return "", errors.New("Please specify only one -H")
   550  	}
   551  
   552  	return dopts.ParseHost(tlsOptions != nil, host)
   553  }
   554  
   555  // UserAgent returns the user agent string used for making API requests
   556  func UserAgent() string {
   557  	return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
   558  }
   559  
   560  var defaultStoreEndpoints = []store.NamedTypeGetter{
   561  	store.EndpointTypeGetter(docker.DockerEndpoint, func() any { return &docker.EndpointMeta{} }),
   562  }
   563  
   564  // RegisterDefaultStoreEndpoints registers a new named endpoint
   565  // metadata type with the default context store config, so that
   566  // endpoint will be supported by stores using the config returned by
   567  // DefaultContextStoreConfig.
   568  func RegisterDefaultStoreEndpoints(ep ...store.NamedTypeGetter) {
   569  	defaultStoreEndpoints = append(defaultStoreEndpoints, ep...)
   570  }
   571  
   572  // DefaultContextStoreConfig returns a new store.Config with the default set of endpoints configured.
   573  func DefaultContextStoreConfig() store.Config {
   574  	return store.NewConfig(
   575  		func() any { return &DockerContext{} },
   576  		defaultStoreEndpoints...,
   577  	)
   578  }