github.com/AliyunContainerService/cli@v0.0.0-20181009023821-814ced4b30d0/cli/command/cli.go (about)

     1  package command
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"net"
     7  	"net/http"
     8  	"os"
     9  	"path/filepath"
    10  	"runtime"
    11  	"time"
    12  
    13  	"github.com/docker/cli/cli"
    14  	"github.com/docker/cli/cli/config"
    15  	cliconfig "github.com/docker/cli/cli/config"
    16  	"github.com/docker/cli/cli/config/configfile"
    17  	"github.com/docker/cli/cli/connhelper"
    18  	cliflags "github.com/docker/cli/cli/flags"
    19  	manifeststore "github.com/docker/cli/cli/manifest/store"
    20  	registryclient "github.com/docker/cli/cli/registry/client"
    21  	"github.com/docker/cli/cli/trust"
    22  	dopts "github.com/docker/cli/opts"
    23  	clitypes "github.com/docker/cli/types"
    24  	"github.com/docker/docker/api"
    25  	"github.com/docker/docker/api/types"
    26  	registrytypes "github.com/docker/docker/api/types/registry"
    27  	"github.com/docker/docker/client"
    28  	"github.com/docker/go-connections/tlsconfig"
    29  	"github.com/pkg/errors"
    30  	"github.com/spf13/cobra"
    31  	"github.com/theupdateframework/notary"
    32  	notaryclient "github.com/theupdateframework/notary/client"
    33  	"github.com/theupdateframework/notary/passphrase"
    34  )
    35  
    36  // Streams is an interface which exposes the standard input and output streams
    37  type Streams interface {
    38  	In() *InStream
    39  	Out() *OutStream
    40  	Err() io.Writer
    41  }
    42  
    43  // Cli represents the docker command line client.
    44  type Cli interface {
    45  	Client() client.APIClient
    46  	Out() *OutStream
    47  	Err() io.Writer
    48  	In() *InStream
    49  	SetIn(in *InStream)
    50  	ConfigFile() *configfile.ConfigFile
    51  	ServerInfo() ServerInfo
    52  	ClientInfo() ClientInfo
    53  	NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
    54  	DefaultVersion() string
    55  	ManifestStore() manifeststore.Store
    56  	RegistryClient(bool) registryclient.RegistryClient
    57  	ContentTrustEnabled() bool
    58  	NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error)
    59  }
    60  
    61  // DockerCli is an instance the docker command line client.
    62  // Instances of the client can be returned from NewDockerCli.
    63  type DockerCli struct {
    64  	configFile            *configfile.ConfigFile
    65  	in                    *InStream
    66  	out                   *OutStream
    67  	err                   io.Writer
    68  	client                client.APIClient
    69  	serverInfo            ServerInfo
    70  	clientInfo            ClientInfo
    71  	contentTrust          bool
    72  	newContainerizeClient func(string) (clitypes.ContainerizedClient, error)
    73  }
    74  
    75  // DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
    76  func (cli *DockerCli) DefaultVersion() string {
    77  	return cli.clientInfo.DefaultVersion
    78  }
    79  
    80  // Client returns the APIClient
    81  func (cli *DockerCli) Client() client.APIClient {
    82  	return cli.client
    83  }
    84  
    85  // Out returns the writer used for stdout
    86  func (cli *DockerCli) Out() *OutStream {
    87  	return cli.out
    88  }
    89  
    90  // Err returns the writer used for stderr
    91  func (cli *DockerCli) Err() io.Writer {
    92  	return cli.err
    93  }
    94  
    95  // SetIn sets the reader used for stdin
    96  func (cli *DockerCli) SetIn(in *InStream) {
    97  	cli.in = in
    98  }
    99  
   100  // In returns the reader used for stdin
   101  func (cli *DockerCli) In() *InStream {
   102  	return cli.in
   103  }
   104  
   105  // ShowHelp shows the command help.
   106  func ShowHelp(err io.Writer) func(*cobra.Command, []string) error {
   107  	return func(cmd *cobra.Command, args []string) error {
   108  		cmd.SetOutput(err)
   109  		cmd.HelpFunc()(cmd, args)
   110  		return nil
   111  	}
   112  }
   113  
   114  // ConfigFile returns the ConfigFile
   115  func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
   116  	return cli.configFile
   117  }
   118  
   119  // ServerInfo returns the server version details for the host this client is
   120  // connected to
   121  func (cli *DockerCli) ServerInfo() ServerInfo {
   122  	return cli.serverInfo
   123  }
   124  
   125  // ClientInfo returns the client details for the cli
   126  func (cli *DockerCli) ClientInfo() ClientInfo {
   127  	return cli.clientInfo
   128  }
   129  
   130  // ContentTrustEnabled returns whether content trust has been enabled by an
   131  // environment variable.
   132  func (cli *DockerCli) ContentTrustEnabled() bool {
   133  	return cli.contentTrust
   134  }
   135  
   136  // ManifestStore returns a store for local manifests
   137  func (cli *DockerCli) ManifestStore() manifeststore.Store {
   138  	// TODO: support override default location from config file
   139  	return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
   140  }
   141  
   142  // RegistryClient returns a client for communicating with a Docker distribution
   143  // registry
   144  func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
   145  	resolver := func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
   146  		return ResolveAuthConfig(ctx, cli, index)
   147  	}
   148  	return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
   149  }
   150  
   151  // Initialize the dockerCli runs initialization that must happen after command
   152  // line flags are parsed.
   153  func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
   154  	cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
   155  
   156  	var err error
   157  	cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
   158  	if tlsconfig.IsErrEncryptedKey(err) {
   159  		passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
   160  		newClient := func(password string) (client.APIClient, error) {
   161  			opts.Common.TLSOptions.Passphrase = password
   162  			return NewAPIClientFromFlags(opts.Common, cli.configFile)
   163  		}
   164  		cli.client, err = getClientWithPassword(passRetriever, newClient)
   165  	}
   166  	if err != nil {
   167  		return err
   168  	}
   169  	var experimentalValue string
   170  	// Environment variable always overrides configuration
   171  	if experimentalValue = os.Getenv("DOCKER_CLI_EXPERIMENTAL"); experimentalValue == "" {
   172  		experimentalValue = cli.configFile.Experimental
   173  	}
   174  	hasExperimental, err := isEnabled(experimentalValue)
   175  	if err != nil {
   176  		return errors.Wrap(err, "Experimental field")
   177  	}
   178  	cli.clientInfo = ClientInfo{
   179  		DefaultVersion:  cli.client.ClientVersion(),
   180  		HasExperimental: hasExperimental,
   181  	}
   182  	cli.initializeFromClient()
   183  	return nil
   184  }
   185  
   186  func isEnabled(value string) (bool, error) {
   187  	switch value {
   188  	case "enabled":
   189  		return true, nil
   190  	case "", "disabled":
   191  		return false, nil
   192  	default:
   193  		return false, errors.Errorf("%q is not valid, should be either enabled or disabled", value)
   194  	}
   195  }
   196  
   197  func (cli *DockerCli) initializeFromClient() {
   198  	ping, err := cli.client.Ping(context.Background())
   199  	if err != nil {
   200  		// Default to true if we fail to connect to daemon
   201  		cli.serverInfo = ServerInfo{HasExperimental: true}
   202  
   203  		if ping.APIVersion != "" {
   204  			cli.client.NegotiateAPIVersionPing(ping)
   205  		}
   206  		return
   207  	}
   208  
   209  	cli.serverInfo = ServerInfo{
   210  		HasExperimental: ping.Experimental,
   211  		OSType:          ping.OSType,
   212  		BuildkitVersion: ping.BuilderVersion,
   213  	}
   214  	cli.client.NegotiateAPIVersionPing(ping)
   215  }
   216  
   217  func getClientWithPassword(passRetriever notary.PassRetriever, newClient func(password string) (client.APIClient, error)) (client.APIClient, error) {
   218  	for attempts := 0; ; attempts++ {
   219  		passwd, giveup, err := passRetriever("private", "encrypted TLS private", false, attempts)
   220  		if giveup || err != nil {
   221  			return nil, errors.Wrap(err, "private key is encrypted, but could not get passphrase")
   222  		}
   223  
   224  		apiclient, err := newClient(passwd)
   225  		if !tlsconfig.IsErrEncryptedKey(err) {
   226  			return apiclient, err
   227  		}
   228  	}
   229  }
   230  
   231  // NotaryClient provides a Notary Repository to interact with signed metadata for an image
   232  func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
   233  	return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
   234  }
   235  
   236  // NewContainerizedEngineClient returns a containerized engine client
   237  func (cli *DockerCli) NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error) {
   238  	return cli.newContainerizeClient(sockPath)
   239  }
   240  
   241  // ServerInfo stores details about the supported features and platform of the
   242  // server
   243  type ServerInfo struct {
   244  	HasExperimental bool
   245  	OSType          string
   246  	BuildkitVersion types.BuilderVersion
   247  }
   248  
   249  // ClientInfo stores details about the supported features of the client
   250  type ClientInfo struct {
   251  	HasExperimental bool
   252  	DefaultVersion  string
   253  }
   254  
   255  // NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
   256  func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool, containerizedFn func(string) (clitypes.ContainerizedClient, error)) *DockerCli {
   257  	return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err, contentTrust: isTrusted, newContainerizeClient: containerizedFn}
   258  }
   259  
   260  // NewAPIClientFromFlags creates a new APIClient from command line flags
   261  func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
   262  	unparsedHost, err := getUnparsedServerHost(opts.Hosts)
   263  	if err != nil {
   264  		return &client.Client{}, err
   265  	}
   266  	var clientOpts []func(*client.Client) error
   267  	helper, err := connhelper.GetConnectionHelper(unparsedHost)
   268  	if err != nil {
   269  		return &client.Client{}, err
   270  	}
   271  	if helper == nil {
   272  		clientOpts = append(clientOpts, withHTTPClient(opts.TLSOptions))
   273  		host, err := dopts.ParseHost(opts.TLSOptions != nil, unparsedHost)
   274  		if err != nil {
   275  			return &client.Client{}, err
   276  		}
   277  		clientOpts = append(clientOpts, client.WithHost(host))
   278  	} else {
   279  		clientOpts = append(clientOpts, func(c *client.Client) error {
   280  			httpClient := &http.Client{
   281  				// No tls
   282  				// No proxy
   283  				Transport: &http.Transport{
   284  					DialContext: helper.Dialer,
   285  				},
   286  			}
   287  			return client.WithHTTPClient(httpClient)(c)
   288  		})
   289  		clientOpts = append(clientOpts, client.WithHost(helper.Host))
   290  		clientOpts = append(clientOpts, client.WithDialContext(helper.Dialer))
   291  	}
   292  
   293  	customHeaders := configFile.HTTPHeaders
   294  	if customHeaders == nil {
   295  		customHeaders = map[string]string{}
   296  	}
   297  	customHeaders["User-Agent"] = UserAgent()
   298  	clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))
   299  
   300  	verStr := api.DefaultVersion
   301  	if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" {
   302  		verStr = tmpStr
   303  	}
   304  	clientOpts = append(clientOpts, client.WithVersion(verStr))
   305  
   306  	return client.NewClientWithOpts(clientOpts...)
   307  }
   308  
   309  func getUnparsedServerHost(hosts []string) (string, error) {
   310  	var host string
   311  	switch len(hosts) {
   312  	case 0:
   313  		host = os.Getenv("DOCKER_HOST")
   314  	case 1:
   315  		host = hosts[0]
   316  	default:
   317  		return "", errors.New("Please specify only one -H")
   318  	}
   319  	return host, nil
   320  }
   321  
   322  func withHTTPClient(tlsOpts *tlsconfig.Options) func(*client.Client) error {
   323  	return func(c *client.Client) error {
   324  		if tlsOpts == nil {
   325  			// Use the default HTTPClient
   326  			return nil
   327  		}
   328  
   329  		opts := *tlsOpts
   330  		opts.ExclusiveRootPools = true
   331  		tlsConfig, err := tlsconfig.Client(opts)
   332  		if err != nil {
   333  			return err
   334  		}
   335  
   336  		httpClient := &http.Client{
   337  			Transport: &http.Transport{
   338  				TLSClientConfig: tlsConfig,
   339  				DialContext: (&net.Dialer{
   340  					KeepAlive: 30 * time.Second,
   341  					Timeout:   30 * time.Second,
   342  				}).DialContext,
   343  			},
   344  			CheckRedirect: client.CheckRedirect,
   345  		}
   346  		return client.WithHTTPClient(httpClient)(c)
   347  	}
   348  }
   349  
   350  // UserAgent returns the user agent string used for making API requests
   351  func UserAgent() string {
   352  	return "Docker-Client/" + cli.Version + " (" + runtime.GOOS + ")"
   353  }