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 }