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 }