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 }