mirror of https://github.com/docker/cli.git
542 lines
16 KiB
Go
542 lines
16 KiB
Go
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
|
//go:build go1.19
|
|
|
|
package command
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/docker/cli/cli/config"
|
|
"github.com/docker/cli/cli/config/configfile"
|
|
dcontext "github.com/docker/cli/cli/context"
|
|
"github.com/docker/cli/cli/context/docker"
|
|
"github.com/docker/cli/cli/context/store"
|
|
"github.com/docker/cli/cli/debug"
|
|
cliflags "github.com/docker/cli/cli/flags"
|
|
manifeststore "github.com/docker/cli/cli/manifest/store"
|
|
registryclient "github.com/docker/cli/cli/registry/client"
|
|
"github.com/docker/cli/cli/streams"
|
|
"github.com/docker/cli/cli/trust"
|
|
"github.com/docker/cli/cli/version"
|
|
dopts "github.com/docker/cli/opts"
|
|
"github.com/docker/docker/api"
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/registry"
|
|
"github.com/docker/docker/api/types/swarm"
|
|
"github.com/docker/docker/client"
|
|
"github.com/docker/go-connections/tlsconfig"
|
|
"github.com/pkg/errors"
|
|
"github.com/spf13/cobra"
|
|
notaryclient "github.com/theupdateframework/notary/client"
|
|
)
|
|
|
|
const defaultInitTimeout = 2 * time.Second
|
|
|
|
// Streams is an interface which exposes the standard input and output streams
|
|
type Streams interface {
|
|
In() *streams.In
|
|
Out() *streams.Out
|
|
Err() io.Writer
|
|
}
|
|
|
|
// Cli represents the docker command line client.
|
|
type Cli interface {
|
|
Client() client.APIClient
|
|
Streams
|
|
SetIn(in *streams.In)
|
|
Apply(ops ...CLIOption) error
|
|
ConfigFile() *configfile.ConfigFile
|
|
ServerInfo() ServerInfo
|
|
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
|
DefaultVersion() string
|
|
CurrentVersion() string
|
|
ManifestStore() manifeststore.Store
|
|
RegistryClient(bool) registryclient.RegistryClient
|
|
ContentTrustEnabled() bool
|
|
BuildKitEnabled() (bool, error)
|
|
ContextStore() store.Store
|
|
CurrentContext() string
|
|
DockerEndpoint() docker.Endpoint
|
|
}
|
|
|
|
// DockerCli is an instance the docker command line client.
|
|
// Instances of the client can be returned from NewDockerCli.
|
|
type DockerCli struct {
|
|
configFile *configfile.ConfigFile
|
|
options *cliflags.ClientOptions
|
|
in *streams.In
|
|
out *streams.Out
|
|
err io.Writer
|
|
client client.APIClient
|
|
serverInfo ServerInfo
|
|
contentTrust bool
|
|
contextStore store.Store
|
|
currentContext string
|
|
init sync.Once
|
|
initErr error
|
|
dockerEndpoint docker.Endpoint
|
|
contextStoreConfig store.Config
|
|
initTimeout time.Duration
|
|
|
|
// baseCtx is the base context used for internal operations. In the future
|
|
// this may be replaced by explicitly passing a context to functions that
|
|
// need it.
|
|
baseCtx context.Context
|
|
}
|
|
|
|
// DefaultVersion returns api.defaultVersion.
|
|
func (cli *DockerCli) DefaultVersion() string {
|
|
return api.DefaultVersion
|
|
}
|
|
|
|
// CurrentVersion returns the API version currently negotiated, or the default
|
|
// version otherwise.
|
|
func (cli *DockerCli) CurrentVersion() string {
|
|
_ = cli.initialize()
|
|
if cli.client == nil {
|
|
return api.DefaultVersion
|
|
}
|
|
return cli.client.ClientVersion()
|
|
}
|
|
|
|
// Client returns the APIClient
|
|
func (cli *DockerCli) Client() client.APIClient {
|
|
if err := cli.initialize(); err != nil {
|
|
_, _ = fmt.Fprintf(cli.Err(), "Failed to initialize: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
return cli.client
|
|
}
|
|
|
|
// Out returns the writer used for stdout
|
|
func (cli *DockerCli) Out() *streams.Out {
|
|
return cli.out
|
|
}
|
|
|
|
// Err returns the writer used for stderr
|
|
func (cli *DockerCli) Err() io.Writer {
|
|
return cli.err
|
|
}
|
|
|
|
// SetIn sets the reader used for stdin
|
|
func (cli *DockerCli) SetIn(in *streams.In) {
|
|
cli.in = in
|
|
}
|
|
|
|
// In returns the reader used for stdin
|
|
func (cli *DockerCli) In() *streams.In {
|
|
return cli.in
|
|
}
|
|
|
|
// ShowHelp shows the command help.
|
|
func ShowHelp(err io.Writer) func(*cobra.Command, []string) error {
|
|
return func(cmd *cobra.Command, args []string) error {
|
|
cmd.SetOut(err)
|
|
cmd.HelpFunc()(cmd, args)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// ConfigFile returns the ConfigFile
|
|
func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
|
|
// TODO(thaJeztah): when would this happen? Is this only in tests (where cli.Initialize() is not called first?)
|
|
if cli.configFile == nil {
|
|
cli.configFile = config.LoadDefaultConfigFile(cli.err)
|
|
}
|
|
return cli.configFile
|
|
}
|
|
|
|
// ServerInfo returns the server version details for the host this client is
|
|
// connected to
|
|
func (cli *DockerCli) ServerInfo() ServerInfo {
|
|
_ = cli.initialize()
|
|
return cli.serverInfo
|
|
}
|
|
|
|
// ContentTrustEnabled returns whether content trust has been enabled by an
|
|
// environment variable.
|
|
func (cli *DockerCli) ContentTrustEnabled() bool {
|
|
return cli.contentTrust
|
|
}
|
|
|
|
// BuildKitEnabled returns buildkit is enabled or not.
|
|
func (cli *DockerCli) BuildKitEnabled() (bool, error) {
|
|
// use DOCKER_BUILDKIT env var value if set and not empty
|
|
if v := os.Getenv("DOCKER_BUILDKIT"); v != "" {
|
|
enabled, err := strconv.ParseBool(v)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
|
|
}
|
|
return enabled, nil
|
|
}
|
|
// if a builder alias is defined, we are using BuildKit
|
|
aliasMap := cli.ConfigFile().Aliases
|
|
if _, ok := aliasMap["builder"]; ok {
|
|
return true, nil
|
|
}
|
|
// otherwise, assume BuildKit is enabled but
|
|
// not if wcow reported from server side
|
|
return cli.ServerInfo().OSType != "windows", nil
|
|
}
|
|
|
|
// ManifestStore returns a store for local manifests
|
|
func (cli *DockerCli) ManifestStore() manifeststore.Store {
|
|
// TODO: support override default location from config file
|
|
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
|
|
}
|
|
|
|
// RegistryClient returns a client for communicating with a Docker distribution
|
|
// registry
|
|
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
|
|
resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig {
|
|
return ResolveAuthConfig(cli.ConfigFile(), index)
|
|
}
|
|
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
|
|
}
|
|
|
|
// WithInitializeClient is passed to DockerCli.Initialize by callers who wish to set a particular API Client for use by the CLI.
|
|
func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) CLIOption {
|
|
return func(dockerCli *DockerCli) error {
|
|
var err error
|
|
dockerCli.client, err = makeClient(dockerCli)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Initialize the dockerCli runs initialization that must happen after command
|
|
// line flags are parsed.
|
|
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error {
|
|
for _, o := range ops {
|
|
if err := o(cli); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
cliflags.SetLogLevel(opts.LogLevel)
|
|
|
|
if opts.ConfigDir != "" {
|
|
config.SetDir(opts.ConfigDir)
|
|
}
|
|
|
|
if opts.Debug {
|
|
debug.Enable()
|
|
}
|
|
if opts.Context != "" && len(opts.Hosts) > 0 {
|
|
return errors.New("conflicting options: either specify --host or --context, not both")
|
|
}
|
|
|
|
cli.options = opts
|
|
cli.configFile = config.LoadDefaultConfigFile(cli.err)
|
|
cli.currentContext = resolveContextName(cli.options, cli.configFile)
|
|
cli.contextStore = &ContextStoreWithDefault{
|
|
Store: store.New(config.ContextStoreDir(), cli.contextStoreConfig),
|
|
Resolver: func() (*DefaultContext, error) {
|
|
return ResolveDefaultContext(cli.options, cli.contextStoreConfig)
|
|
},
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// NewAPIClientFromFlags creates a new APIClient from command line flags
|
|
func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
|
|
if opts.Context != "" && len(opts.Hosts) > 0 {
|
|
return nil, errors.New("conflicting options: either specify --host or --context, not both")
|
|
}
|
|
|
|
storeConfig := DefaultContextStoreConfig()
|
|
contextStore := &ContextStoreWithDefault{
|
|
Store: store.New(config.ContextStoreDir(), storeConfig),
|
|
Resolver: func() (*DefaultContext, error) {
|
|
return ResolveDefaultContext(opts, storeConfig)
|
|
},
|
|
}
|
|
endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "unable to resolve docker endpoint")
|
|
}
|
|
return newAPIClientFromEndpoint(endpoint, configFile)
|
|
}
|
|
|
|
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
|
|
opts, err := ep.ClientOpts()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(configFile.HTTPHeaders) > 0 {
|
|
opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders))
|
|
}
|
|
opts = append(opts, client.WithUserAgent(UserAgent()))
|
|
return client.NewClientWithOpts(opts...)
|
|
}
|
|
|
|
func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) {
|
|
if s == nil {
|
|
return docker.Endpoint{}, fmt.Errorf("no context store initialized")
|
|
}
|
|
ctxMeta, err := s.GetMetadata(contextName)
|
|
if err != nil {
|
|
return docker.Endpoint{}, err
|
|
}
|
|
epMeta, err := docker.EndpointFromContext(ctxMeta)
|
|
if err != nil {
|
|
return docker.Endpoint{}, err
|
|
}
|
|
return docker.WithTLSData(s, contextName, epMeta)
|
|
}
|
|
|
|
// Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
|
|
func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) {
|
|
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
|
|
if err != nil {
|
|
return docker.Endpoint{}, err
|
|
}
|
|
|
|
var (
|
|
skipTLSVerify bool
|
|
tlsData *dcontext.TLSData
|
|
)
|
|
|
|
if opts.TLSOptions != nil {
|
|
skipTLSVerify = opts.TLSOptions.InsecureSkipVerify
|
|
tlsData, err = dcontext.TLSDataFromFiles(opts.TLSOptions.CAFile, opts.TLSOptions.CertFile, opts.TLSOptions.KeyFile)
|
|
if err != nil {
|
|
return docker.Endpoint{}, err
|
|
}
|
|
}
|
|
|
|
return docker.Endpoint{
|
|
EndpointMeta: docker.EndpointMeta{
|
|
Host: host,
|
|
SkipTLSVerify: skipTLSVerify,
|
|
},
|
|
TLSData: tlsData,
|
|
}, nil
|
|
}
|
|
|
|
func (cli *DockerCli) getInitTimeout() time.Duration {
|
|
if cli.initTimeout != 0 {
|
|
return cli.initTimeout
|
|
}
|
|
return defaultInitTimeout
|
|
}
|
|
|
|
func (cli *DockerCli) initializeFromClient() {
|
|
ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout())
|
|
defer cancel()
|
|
|
|
ping, err := cli.client.Ping(ctx)
|
|
if err != nil {
|
|
// Default to true if we fail to connect to daemon
|
|
cli.serverInfo = ServerInfo{HasExperimental: true}
|
|
|
|
if ping.APIVersion != "" {
|
|
cli.client.NegotiateAPIVersionPing(ping)
|
|
}
|
|
return
|
|
}
|
|
|
|
cli.serverInfo = ServerInfo{
|
|
HasExperimental: ping.Experimental,
|
|
OSType: ping.OSType,
|
|
BuildkitVersion: ping.BuilderVersion,
|
|
SwarmStatus: ping.SwarmStatus,
|
|
}
|
|
cli.client.NegotiateAPIVersionPing(ping)
|
|
}
|
|
|
|
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
|
|
func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
|
|
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
|
|
}
|
|
|
|
// ContextStore returns the ContextStore
|
|
func (cli *DockerCli) ContextStore() store.Store {
|
|
return cli.contextStore
|
|
}
|
|
|
|
// CurrentContext returns the current context name, based on flags,
|
|
// environment variables and the cli configuration file, in the following
|
|
// order of preference:
|
|
//
|
|
// 1. The "--context" command-line option.
|
|
// 2. The "DOCKER_CONTEXT" environment variable ([EnvOverrideContext]).
|
|
// 3. The current context as configured through the in "currentContext"
|
|
// field in the CLI configuration file ("~/.docker/config.json").
|
|
// 4. If no context is configured, use the "default" context.
|
|
//
|
|
// # Fallbacks for backward-compatibility
|
|
//
|
|
// To preserve backward-compatibility with the "pre-contexts" behavior,
|
|
// the "default" context is used if:
|
|
//
|
|
// - The "--host" option is set
|
|
// - The "DOCKER_HOST" ([client.EnvOverrideHost]) environment variable is set
|
|
// to a non-empty value.
|
|
//
|
|
// In these cases, the default context is used, which uses the host as
|
|
// specified in "DOCKER_HOST", and TLS config from flags/env vars.
|
|
//
|
|
// Setting both the "--context" and "--host" flags is ambiguous and results
|
|
// in an error when the cli is started.
|
|
//
|
|
// CurrentContext does not validate if the given context exists or if it's
|
|
// valid; errors may occur when trying to use it.
|
|
func (cli *DockerCli) CurrentContext() string {
|
|
return cli.currentContext
|
|
}
|
|
|
|
// CurrentContext returns the current context name, based on flags,
|
|
// environment variables and the cli configuration file. It does not
|
|
// validate if the given context exists or if it's valid; errors may
|
|
// occur when trying to use it.
|
|
//
|
|
// Refer to [DockerCli.CurrentContext] above for further details.
|
|
func resolveContextName(opts *cliflags.ClientOptions, cfg *configfile.ConfigFile) string {
|
|
if opts != nil && opts.Context != "" {
|
|
return opts.Context
|
|
}
|
|
if opts != nil && len(opts.Hosts) > 0 {
|
|
return DefaultContextName
|
|
}
|
|
if os.Getenv(client.EnvOverrideHost) != "" {
|
|
return DefaultContextName
|
|
}
|
|
if ctxName := os.Getenv(EnvOverrideContext); ctxName != "" {
|
|
return ctxName
|
|
}
|
|
if cfg != nil && cfg.CurrentContext != "" {
|
|
// We don't validate if this context exists: errors may occur when trying to use it.
|
|
return cfg.CurrentContext
|
|
}
|
|
return DefaultContextName
|
|
}
|
|
|
|
// DockerEndpoint returns the current docker endpoint
|
|
func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
|
|
if err := cli.initialize(); err != nil {
|
|
// Note that we're not terminating here, as this function may be used
|
|
// in cases where we're able to continue.
|
|
_, _ = fmt.Fprintf(cli.Err(), "%v\n", cli.initErr)
|
|
}
|
|
return cli.dockerEndpoint
|
|
}
|
|
|
|
func (cli *DockerCli) getDockerEndPoint() (ep docker.Endpoint, err error) {
|
|
cn := cli.CurrentContext()
|
|
if cn == DefaultContextName {
|
|
return resolveDefaultDockerEndpoint(cli.options)
|
|
}
|
|
return resolveDockerEndpoint(cli.contextStore, cn)
|
|
}
|
|
|
|
func (cli *DockerCli) initialize() error {
|
|
cli.init.Do(func() {
|
|
cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint()
|
|
if cli.initErr != nil {
|
|
cli.initErr = errors.Wrap(cli.initErr, "unable to resolve docker endpoint")
|
|
return
|
|
}
|
|
if cli.client == nil {
|
|
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil {
|
|
return
|
|
}
|
|
}
|
|
if cli.baseCtx == nil {
|
|
cli.baseCtx = context.Background()
|
|
}
|
|
cli.initializeFromClient()
|
|
})
|
|
return cli.initErr
|
|
}
|
|
|
|
// Apply all the operation on the cli
|
|
func (cli *DockerCli) Apply(ops ...CLIOption) error {
|
|
for _, op := range ops {
|
|
if err := op(cli); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ServerInfo stores details about the supported features and platform of the
|
|
// server
|
|
type ServerInfo struct {
|
|
HasExperimental bool
|
|
OSType string
|
|
BuildkitVersion types.BuilderVersion
|
|
|
|
// SwarmStatus provides information about the current swarm status of the
|
|
// engine, obtained from the "Swarm" header in the API response.
|
|
//
|
|
// It can be a nil struct if the API version does not provide this header
|
|
// in the ping response, or if an error occurred, in which case the client
|
|
// should use other ways to get the current swarm status, such as the /swarm
|
|
// endpoint.
|
|
SwarmStatus *swarm.Status
|
|
}
|
|
|
|
// NewDockerCli returns a DockerCli instance with all operators applied on it.
|
|
// It applies by default the standard streams, and the content trust from
|
|
// environment.
|
|
func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
|
|
defaultOps := []CLIOption{
|
|
WithContentTrustFromEnv(),
|
|
WithDefaultContextStoreConfig(),
|
|
WithStandardStreams(),
|
|
}
|
|
ops = append(defaultOps, ops...)
|
|
|
|
cli := &DockerCli{baseCtx: context.Background()}
|
|
if err := cli.Apply(ops...); err != nil {
|
|
return nil, err
|
|
}
|
|
return cli, nil
|
|
}
|
|
|
|
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
|
|
var host string
|
|
switch len(hosts) {
|
|
case 0:
|
|
host = os.Getenv(client.EnvOverrideHost)
|
|
case 1:
|
|
host = hosts[0]
|
|
default:
|
|
return "", errors.New("Please specify only one -H")
|
|
}
|
|
|
|
return dopts.ParseHost(tlsOptions != nil, host)
|
|
}
|
|
|
|
// UserAgent returns the user agent string used for making API requests
|
|
func UserAgent() string {
|
|
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
|
|
}
|
|
|
|
var defaultStoreEndpoints = []store.NamedTypeGetter{
|
|
store.EndpointTypeGetter(docker.DockerEndpoint, func() any { return &docker.EndpointMeta{} }),
|
|
}
|
|
|
|
// RegisterDefaultStoreEndpoints registers a new named endpoint
|
|
// metadata type with the default context store config, so that
|
|
// endpoint will be supported by stores using the config returned by
|
|
// DefaultContextStoreConfig.
|
|
func RegisterDefaultStoreEndpoints(ep ...store.NamedTypeGetter) {
|
|
defaultStoreEndpoints = append(defaultStoreEndpoints, ep...)
|
|
}
|
|
|
|
// DefaultContextStoreConfig returns a new store.Config with the default set of endpoints configured.
|
|
func DefaultContextStoreConfig() store.Config {
|
|
return store.NewConfig(
|
|
func() any { return &DockerContext{} },
|
|
defaultStoreEndpoints...,
|
|
)
|
|
}
|