github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/command/cli.go (about)

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