github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/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/docker/api" 18 "github.com/docker/docker/api/types" 19 "github.com/docker/docker/api/types/registry" 20 "github.com/docker/docker/api/types/swarm" 21 "github.com/docker/docker/client" 22 "github.com/khulnasoft-lab/go-connections/tlsconfig" 23 "github.com/khulnasoft/cli/cli/config" 24 "github.com/khulnasoft/cli/cli/config/configfile" 25 dcontext "github.com/khulnasoft/cli/cli/context" 26 "github.com/khulnasoft/cli/cli/context/docker" 27 "github.com/khulnasoft/cli/cli/context/store" 28 "github.com/khulnasoft/cli/cli/debug" 29 cliflags "github.com/khulnasoft/cli/cli/flags" 30 manifeststore "github.com/khulnasoft/cli/cli/manifest/store" 31 registryclient "github.com/khulnasoft/cli/cli/registry/client" 32 "github.com/khulnasoft/cli/cli/streams" 33 "github.com/khulnasoft/cli/cli/trust" 34 "github.com/khulnasoft/cli/cli/version" 35 dopts "github.com/khulnasoft/cli/opts" 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 return nil 277 } 278 279 // NewAPIClientFromFlags creates a new APIClient from command line flags 280 func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.ConfigFile) (client.APIClient, error) { 281 if opts.Context != "" && len(opts.Hosts) > 0 { 282 return nil, errors.New("conflicting options: either specify --host or --context, not both") 283 } 284 285 storeConfig := DefaultContextStoreConfig() 286 contextStore := &ContextStoreWithDefault{ 287 Store: store.New(config.ContextStoreDir(), storeConfig), 288 Resolver: func() (*DefaultContext, error) { 289 return ResolveDefaultContext(opts, storeConfig) 290 }, 291 } 292 endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile)) 293 if err != nil { 294 return nil, errors.Wrap(err, "unable to resolve docker endpoint") 295 } 296 return newAPIClientFromEndpoint(endpoint, configFile) 297 } 298 299 func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) { 300 opts, err := ep.ClientOpts() 301 if err != nil { 302 return nil, err 303 } 304 if len(configFile.HTTPHeaders) > 0 { 305 opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders)) 306 } 307 opts = append(opts, client.WithUserAgent(UserAgent())) 308 return client.NewClientWithOpts(opts...) 309 } 310 311 func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) { 312 if s == nil { 313 return docker.Endpoint{}, fmt.Errorf("no context store initialized") 314 } 315 ctxMeta, err := s.GetMetadata(contextName) 316 if err != nil { 317 return docker.Endpoint{}, err 318 } 319 epMeta, err := docker.EndpointFromContext(ctxMeta) 320 if err != nil { 321 return docker.Endpoint{}, err 322 } 323 return docker.WithTLSData(s, contextName, epMeta) 324 } 325 326 // Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags) 327 func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) { 328 host, err := getServerHost(opts.Hosts, opts.TLSOptions) 329 if err != nil { 330 return docker.Endpoint{}, err 331 } 332 333 var ( 334 skipTLSVerify bool 335 tlsData *dcontext.TLSData 336 ) 337 338 if opts.TLSOptions != nil { 339 skipTLSVerify = opts.TLSOptions.InsecureSkipVerify 340 tlsData, err = dcontext.TLSDataFromFiles(opts.TLSOptions.CAFile, opts.TLSOptions.CertFile, opts.TLSOptions.KeyFile) 341 if err != nil { 342 return docker.Endpoint{}, err 343 } 344 } 345 346 return docker.Endpoint{ 347 EndpointMeta: docker.EndpointMeta{ 348 Host: host, 349 SkipTLSVerify: skipTLSVerify, 350 }, 351 TLSData: tlsData, 352 }, nil 353 } 354 355 func (cli *DockerCli) getInitTimeout() time.Duration { 356 if cli.initTimeout != 0 { 357 return cli.initTimeout 358 } 359 return defaultInitTimeout 360 } 361 362 func (cli *DockerCli) initializeFromClient() { 363 ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout()) 364 defer cancel() 365 366 ping, err := cli.client.Ping(ctx) 367 if err != nil { 368 // Default to true if we fail to connect to daemon 369 cli.serverInfo = ServerInfo{HasExperimental: true} 370 371 if ping.APIVersion != "" { 372 cli.client.NegotiateAPIVersionPing(ping) 373 } 374 return 375 } 376 377 cli.serverInfo = ServerInfo{ 378 HasExperimental: ping.Experimental, 379 OSType: ping.OSType, 380 BuildkitVersion: ping.BuilderVersion, 381 SwarmStatus: ping.SwarmStatus, 382 } 383 cli.client.NegotiateAPIVersionPing(ping) 384 } 385 386 // NotaryClient provides a Notary Repository to interact with signed metadata for an image 387 func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) { 388 return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...) 389 } 390 391 // ContextStore returns the ContextStore 392 func (cli *DockerCli) ContextStore() store.Store { 393 return cli.contextStore 394 } 395 396 // CurrentContext returns the current context name, based on flags, 397 // environment variables and the cli configuration file, in the following 398 // order of preference: 399 // 400 // 1. The "--context" command-line option. 401 // 2. The "DOCKER_CONTEXT" environment variable ([EnvOverrideContext]). 402 // 3. The current context as configured through the in "currentContext" 403 // field in the CLI configuration file ("~/.docker/config.json"). 404 // 4. If no context is configured, use the "default" context. 405 // 406 // # Fallbacks for backward-compatibility 407 // 408 // To preserve backward-compatibility with the "pre-contexts" behavior, 409 // the "default" context is used if: 410 // 411 // - The "--host" option is set 412 // - The "DOCKER_HOST" ([client.EnvOverrideHost]) environment variable is set 413 // to a non-empty value. 414 // 415 // In these cases, the default context is used, which uses the host as 416 // specified in "DOCKER_HOST", and TLS config from flags/env vars. 417 // 418 // Setting both the "--context" and "--host" flags is ambiguous and results 419 // in an error when the cli is started. 420 // 421 // CurrentContext does not validate if the given context exists or if it's 422 // valid; errors may occur when trying to use it. 423 func (cli *DockerCli) CurrentContext() string { 424 return cli.currentContext 425 } 426 427 // CurrentContext returns the current context name, based on flags, 428 // environment variables and the cli configuration file. It does not 429 // validate if the given context exists or if it's valid; errors may 430 // occur when trying to use it. 431 // 432 // Refer to [DockerCli.CurrentContext] above for further details. 433 func resolveContextName(opts *cliflags.ClientOptions, cfg *configfile.ConfigFile) string { 434 if opts != nil && opts.Context != "" { 435 return opts.Context 436 } 437 if opts != nil && len(opts.Hosts) > 0 { 438 return DefaultContextName 439 } 440 if os.Getenv(client.EnvOverrideHost) != "" { 441 return DefaultContextName 442 } 443 if ctxName := os.Getenv(EnvOverrideContext); ctxName != "" { 444 return ctxName 445 } 446 if cfg != nil && cfg.CurrentContext != "" { 447 // We don't validate if this context exists: errors may occur when trying to use it. 448 return cfg.CurrentContext 449 } 450 return DefaultContextName 451 } 452 453 // DockerEndpoint returns the current docker endpoint 454 func (cli *DockerCli) DockerEndpoint() docker.Endpoint { 455 if err := cli.initialize(); err != nil { 456 // Note that we're not terminating here, as this function may be used 457 // in cases where we're able to continue. 458 _, _ = fmt.Fprintf(cli.Err(), "%v\n", cli.initErr) 459 } 460 return cli.dockerEndpoint 461 } 462 463 func (cli *DockerCli) getDockerEndPoint() (ep docker.Endpoint, err error) { 464 cn := cli.CurrentContext() 465 if cn == DefaultContextName { 466 return resolveDefaultDockerEndpoint(cli.options) 467 } 468 return resolveDockerEndpoint(cli.contextStore, cn) 469 } 470 471 func (cli *DockerCli) initialize() error { 472 cli.init.Do(func() { 473 cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint() 474 if cli.initErr != nil { 475 cli.initErr = errors.Wrap(cli.initErr, "unable to resolve docker endpoint") 476 return 477 } 478 if cli.client == nil { 479 if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil { 480 return 481 } 482 } 483 if cli.baseCtx == nil { 484 cli.baseCtx = context.Background() 485 } 486 cli.initializeFromClient() 487 }) 488 return cli.initErr 489 } 490 491 // Apply all the operation on the cli 492 func (cli *DockerCli) Apply(ops ...CLIOption) error { 493 for _, op := range ops { 494 if err := op(cli); err != nil { 495 return err 496 } 497 } 498 return nil 499 } 500 501 // ServerInfo stores details about the supported features and platform of the 502 // server 503 type ServerInfo struct { 504 HasExperimental bool 505 OSType string 506 BuildkitVersion types.BuilderVersion 507 508 // SwarmStatus provides information about the current swarm status of the 509 // engine, obtained from the "Swarm" header in the API response. 510 // 511 // It can be a nil struct if the API version does not provide this header 512 // in the ping response, or if an error occurred, in which case the client 513 // should use other ways to get the current swarm status, such as the /swarm 514 // endpoint. 515 SwarmStatus *swarm.Status 516 } 517 518 // NewDockerCli returns a DockerCli instance with all operators applied on it. 519 // It applies by default the standard streams, and the content trust from 520 // environment. 521 func NewDockerCli(ops ...CLIOption) (*DockerCli, error) { 522 defaultOps := []CLIOption{ 523 WithContentTrustFromEnv(), 524 WithDefaultContextStoreConfig(), 525 WithStandardStreams(), 526 } 527 ops = append(defaultOps, ops...) 528 529 cli := &DockerCli{baseCtx: context.Background()} 530 if err := cli.Apply(ops...); err != nil { 531 return nil, err 532 } 533 return cli, nil 534 } 535 536 func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) { 537 var host string 538 switch len(hosts) { 539 case 0: 540 host = os.Getenv(client.EnvOverrideHost) 541 case 1: 542 host = hosts[0] 543 default: 544 return "", errors.New("Please specify only one -H") 545 } 546 547 return dopts.ParseHost(tlsOptions != nil, host) 548 } 549 550 // UserAgent returns the user agent string used for making API requests 551 func UserAgent() string { 552 return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")" 553 } 554 555 var defaultStoreEndpoints = []store.NamedTypeGetter{ 556 store.EndpointTypeGetter(docker.DockerEndpoint, func() any { return &docker.EndpointMeta{} }), 557 } 558 559 // RegisterDefaultStoreEndpoints registers a new named endpoint 560 // metadata type with the default context store config, so that 561 // endpoint will be supported by stores using the config returned by 562 // DefaultContextStoreConfig. 563 func RegisterDefaultStoreEndpoints(ep ...store.NamedTypeGetter) { 564 defaultStoreEndpoints = append(defaultStoreEndpoints, ep...) 565 } 566 567 // DefaultContextStoreConfig returns a new store.Config with the default set of endpoints configured. 568 func DefaultContextStoreConfig() store.Config { 569 return store.NewConfig( 570 func() any { return &DockerContext{} }, 571 defaultStoreEndpoints..., 572 ) 573 }