diff --git a/Dockerfile b/Dockerfile index 27608c4f5f..1f25c92255 100644 --- a/Dockerfile +++ b/Dockerfile @@ -241,11 +241,12 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \ hello-world:latest@sha256:c5515758d4c5e1e838e9cd307f6c6a0d620b5e07e6f927b07d05f6d12a1ac8d7 # See also ensureFrozenImagesLinux() in "integration-cli/fixtures_linux_daemon_test.go" (which needs to be updated when adding images to this list) -# Install tomlv, vndr, runc, containerd, tini, docker-proxy +# Install tomlv, vndr, runc, containerd, tini, docker-proxy dockercli # Please edit hack/dockerfile/install-binaries.sh to update them. COPY hack/dockerfile/binaries-commits /tmp/binaries-commits COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh -RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy bindata +RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy bindata dockercli +ENV PATH=/usr/local/cli:$PATH # Wrap all commands in the "docker-in-docker" script to allow nested containers ENTRYPOINT ["hack/dind"] diff --git a/Dockerfile.aarch64 b/Dockerfile.aarch64 index 02782eeeb6..e70853e95e 100644 --- a/Dockerfile.aarch64 +++ b/Dockerfile.aarch64 @@ -192,7 +192,8 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \ # Please edit hack/dockerfile/install-binaries.sh to update them. COPY hack/dockerfile/binaries-commits /tmp/binaries-commits COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh -RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy +RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy dockercli +ENV PATH=/usr/local/cli:$PATH # Wrap all commands in the "docker-in-docker" script to allow nested containers ENTRYPOINT ["hack/dind"] diff --git a/Dockerfile.armhf b/Dockerfile.armhf index b972c1b087..c12be6d488 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -173,7 +173,8 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \ # Please edit hack/dockerfile/install-binaries.sh to update them. COPY hack/dockerfile/binaries-commits /tmp/binaries-commits COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh -RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy +RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy dockercli +ENV PATH=/usr/local/cli:$PATH ENTRYPOINT ["hack/dind"] diff --git a/Dockerfile.ppc64le b/Dockerfile.ppc64le index d4fd8fd491..f240cc1f82 100644 --- a/Dockerfile.ppc64le +++ b/Dockerfile.ppc64le @@ -179,7 +179,8 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \ # Please edit hack/dockerfile/install-binaries.sh to update them. COPY hack/dockerfile/binaries-commits /tmp/binaries-commits COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh -RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy +RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy dockercli +ENV PATH=/usr/local/cli:$PATH # Wrap all commands in the "docker-in-docker" script to allow nested containers ENTRYPOINT ["hack/dind"] diff --git a/Dockerfile.s390x b/Dockerfile.s390x index 4d9af91806..f82ebb4f92 100644 --- a/Dockerfile.s390x +++ b/Dockerfile.s390x @@ -172,7 +172,8 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \ # Please edit hack/dockerfile/install-binaries.sh to update them. COPY hack/dockerfile/binaries-commits /tmp/binaries-commits COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh -RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy +RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy dockercli +ENV PATH=/usr/local/cli:$PATH # Wrap all commands in the "docker-in-docker" script to allow nested containers ENTRYPOINT ["hack/dind"] diff --git a/Dockerfile.simple b/Dockerfile.simple index 248f88de3e..512828e5ed 100644 --- a/Dockerfile.simple +++ b/Dockerfile.simple @@ -64,7 +64,8 @@ ENV CGO_LDFLAGS -L/lib # Please edit hack/dockerfile/install-binaries.sh to update them. COPY hack/dockerfile/binaries-commits /tmp/binaries-commits COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh -RUN /tmp/install-binaries.sh runc containerd tini proxy +RUN /tmp/install-binaries.sh runc containerd tini proxy dockercli +ENV PATH=/usr/local/cli:$PATH ENV AUTO_GOPATH 1 WORKDIR /usr/src/docker diff --git a/Makefile b/Makefile index 9fa993b635..977091ba0d 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ DOCKER_INCREMENTAL_BINARY := $(if $(DOCKER_INCREMENTAL_BINARY),$(DOCKER_INCREMEN export DOCKER_INCREMENTAL_BINARY # get OS/Arch of docker engine -DOCKER_OSARCH := $(shell bash -c 'source hack/make/.detect-daemon-osarch && echo $${DOCKER_ENGINE_OSARCH:-$$DOCKER_CLIENT_OSARCH}') +DOCKER_OSARCH := $(shell bash -c 'source hack/make/.detect-daemon-osarch && echo $${DOCKER_ENGINE_OSARCH}') DOCKERFILE := $(shell bash -c 'source hack/make/.detect-daemon-osarch && echo $${DOCKERFILE}') DOCKER_GITCOMMIT := $(shell git rev-parse --short HEAD || echo unsupported) @@ -24,6 +24,7 @@ DOCKER_ENVS := \ -e DOCKER_BUILD_ARGS \ -e DOCKER_BUILD_GOGC \ -e DOCKER_BUILD_PKGS \ + -e DOCKER_CLI_PATH \ -e DOCKER_DEBUG \ -e DOCKER_EXPERIMENTAL \ -e DOCKER_GITCOMMIT \ @@ -63,7 +64,8 @@ PKGCACHE_MAP := gopath:/go/pkg goroot-linux_amd64:/usr/local/go/pkg/linux_amd64 PKGCACHE_VOLROOT := dockerdev-go-pkg-cache PKGCACHE_VOL := $(if $(PKGCACHE_DIR),$(CURDIR)/$(PKGCACHE_DIR)/,$(PKGCACHE_VOLROOT)-) DOCKER_MOUNT_PKGCACHE := $(if $(DOCKER_INCREMENTAL_BINARY),$(shell echo $(PKGCACHE_MAP) | sed -E 's@([^ ]*)@-v "$(PKGCACHE_VOL)\1"@g'),) -DOCKER_MOUNT := $(DOCKER_MOUNT) $(DOCKER_MOUNT_PKGCACHE) +DOCKER_MOUNT_CLI := $(if $(DOCKER_CLI_PATH),-v $(shell dirname $(DOCKER_CLI_PATH)):/usr/local/cli,) +DOCKER_MOUNT := $(DOCKER_MOUNT) $(DOCKER_MOUNT_PKGCACHE) $(DOCKER_MOUNT_CLI) GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null) GIT_BRANCH_CLEAN := $(shell echo $(GIT_BRANCH) | sed -e "s/[^[:alnum:]]/-/g") @@ -79,6 +81,11 @@ SWAGGER_DOCS_PORT ?= 9000 INTEGRATION_CLI_MASTER_IMAGE := $(if $(INTEGRATION_CLI_MASTER_IMAGE), $(INTEGRATION_CLI_MASTER_IMAGE), integration-cli-master) INTEGRATION_CLI_WORKER_IMAGE := $(if $(INTEGRATION_CLI_WORKER_IMAGE), $(INTEGRATION_CLI_WORKER_IMAGE), integration-cli-worker) +define \n + + +endef + # if this session isn't interactive, then we don't want to allocate a # TTY, which would fail, but if it is interactive, we do want to attach # so that the user can send e.g. ^C through. @@ -98,6 +105,7 @@ binary: build ## build the linux binaries $(DOCKER_RUN_DOCKER) hack/make.sh binary build: bundles init-go-pkg-cache + $(warning The docker client CLI has moved to github.com/docker/cli. By default, it is built from the git sha specified in hack/dockerfile/binaries-commits. For a dev-test cycle involving the CLI, run:${\n} DOCKER_CLI_PATH=/host/path/to/cli/binary make shell ${\n} then change the cli and compile into a binary at the same location.${\n}) docker build ${BUILD_APT_MIRROR} ${DOCKER_BUILD_ARGS} -t "$(DOCKER_IMAGE)" -f "$(DOCKERFILE)" . bundles: diff --git a/cli/cli.go b/cli/cli.go new file mode 100644 index 0000000000..4281667192 --- /dev/null +++ b/cli/cli.go @@ -0,0 +1,25 @@ +package cli + +import ( + "os" + "path/filepath" + + "github.com/docker/docker/pkg/homedir" +) + +var ( + configDir = os.Getenv("DOCKER_CONFIG") + configFileDir = ".docker" +) + +// ConfigurationDir returns the path to the configuration directory as specified by the DOCKER_CONFIG environment variable. +// TODO: this was copied from cli/config/configfile and should be removed once cmd/dockerd moves +func ConfigurationDir() string { + return configDir +} + +func init() { + if configDir == "" { + configDir = filepath.Join(homedir.Get(), configFileDir) + } +} diff --git a/cli/command/bundlefile/bundlefile.go b/cli/command/bundlefile/bundlefile.go deleted file mode 100644 index 07e2c8b081..0000000000 --- a/cli/command/bundlefile/bundlefile.go +++ /dev/null @@ -1,70 +0,0 @@ -package bundlefile - -import ( - "encoding/json" - "io" - - "github.com/pkg/errors" -) - -// Bundlefile stores the contents of a bundlefile -type Bundlefile struct { - Version string - Services map[string]Service -} - -// Service is a service from a bundlefile -type Service struct { - Image string - Command []string `json:",omitempty"` - Args []string `json:",omitempty"` - Env []string `json:",omitempty"` - Labels map[string]string `json:",omitempty"` - Ports []Port `json:",omitempty"` - WorkingDir *string `json:",omitempty"` - User *string `json:",omitempty"` - Networks []string `json:",omitempty"` -} - -// Port is a port as defined in a bundlefile -type Port struct { - Protocol string - Port uint32 -} - -// LoadFile loads a bundlefile from a path to the file -func LoadFile(reader io.Reader) (*Bundlefile, error) { - bundlefile := &Bundlefile{} - - decoder := json.NewDecoder(reader) - if err := decoder.Decode(bundlefile); err != nil { - switch jsonErr := err.(type) { - case *json.SyntaxError: - return nil, errors.Errorf( - "JSON syntax error at byte %v: %s", - jsonErr.Offset, - jsonErr.Error()) - case *json.UnmarshalTypeError: - return nil, errors.Errorf( - "Unexpected type at byte %v. Expected %s but received %s.", - jsonErr.Offset, - jsonErr.Type, - jsonErr.Value) - } - return nil, err - } - - return bundlefile, nil -} - -// Print writes the contents of the bundlefile to the output writer -// as human readable json -func Print(out io.Writer, bundle *Bundlefile) error { - bytes, err := json.MarshalIndent(*bundle, "", " ") - if err != nil { - return err - } - - _, err = out.Write(bytes) - return err -} diff --git a/cli/command/bundlefile/bundlefile_test.go b/cli/command/bundlefile/bundlefile_test.go deleted file mode 100644 index bd059c4dca..0000000000 --- a/cli/command/bundlefile/bundlefile_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package bundlefile - -import ( - "bytes" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLoadFileV01Success(t *testing.T) { - reader := strings.NewReader(`{ - "Version": "0.1", - "Services": { - "redis": { - "Image": "redis@sha256:4b24131101fa0117bcaa18ac37055fffd9176aa1a240392bb8ea85e0be50f2ce", - "Networks": ["default"] - }, - "web": { - "Image": "dockercloud/hello-world@sha256:fe79a2cfbd17eefc344fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d", - "Networks": ["default"], - "User": "web" - } - } - }`) - - bundle, err := LoadFile(reader) - assert.NoError(t, err) - assert.Equal(t, "0.1", bundle.Version) - assert.Len(t, bundle.Services, 2) -} - -func TestLoadFileSyntaxError(t *testing.T) { - reader := strings.NewReader(`{ - "Version": "0.1", - "Services": unquoted string - }`) - - _, err := LoadFile(reader) - assert.EqualError(t, err, "JSON syntax error at byte 37: invalid character 'u' looking for beginning of value") -} - -func TestLoadFileTypeError(t *testing.T) { - reader := strings.NewReader(`{ - "Version": "0.1", - "Services": { - "web": { - "Image": "redis", - "Networks": "none" - } - } - }`) - - _, err := LoadFile(reader) - assert.EqualError(t, err, "Unexpected type at byte 94. Expected []string but received string.") -} - -func TestPrint(t *testing.T) { - var buffer bytes.Buffer - bundle := &Bundlefile{ - Version: "0.1", - Services: map[string]Service{ - "web": { - Image: "image", - Command: []string{"echo", "something"}, - }, - }, - } - assert.NoError(t, Print(&buffer, bundle)) - output := buffer.String() - assert.Contains(t, output, "\"Image\": \"image\"") - assert.Contains(t, output, - `"Command": [ - "echo", - "something" - ]`) -} diff --git a/cli/command/checkpoint/cmd.go b/cli/command/checkpoint/cmd.go deleted file mode 100644 index d5705a4dad..0000000000 --- a/cli/command/checkpoint/cmd.go +++ /dev/null @@ -1,24 +0,0 @@ -package checkpoint - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -// NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental) -func NewCheckpointCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "checkpoint", - Short: "Manage checkpoints", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - Tags: map[string]string{"experimental": "", "version": "1.25"}, - } - cmd.AddCommand( - newCreateCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/checkpoint/create.go b/cli/command/checkpoint/create.go deleted file mode 100644 index 473a941733..0000000000 --- a/cli/command/checkpoint/create.go +++ /dev/null @@ -1,58 +0,0 @@ -package checkpoint - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -type createOptions struct { - container string - checkpoint string - checkpointDir string - leaveRunning bool -} - -func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts createOptions - - cmd := &cobra.Command{ - Use: "create [OPTIONS] CONTAINER CHECKPOINT", - Short: "Create a checkpoint from a running container", - Args: cli.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - opts.container = args[0] - opts.checkpoint = args[1] - return runCreate(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVar(&opts.leaveRunning, "leave-running", false, "Leave the container running after checkpoint") - flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory") - - return cmd -} - -func runCreate(dockerCli *command.DockerCli, opts createOptions) error { - client := dockerCli.Client() - - checkpointOpts := types.CheckpointCreateOptions{ - CheckpointID: opts.checkpoint, - CheckpointDir: opts.checkpointDir, - Exit: !opts.leaveRunning, - } - - err := client.CheckpointCreate(context.Background(), opts.container, checkpointOpts) - if err != nil { - return err - } - - fmt.Fprintf(dockerCli.Out(), "%s\n", opts.checkpoint) - return nil -} diff --git a/cli/command/checkpoint/list.go b/cli/command/checkpoint/list.go deleted file mode 100644 index 20e7d6d73a..0000000000 --- a/cli/command/checkpoint/list.go +++ /dev/null @@ -1,54 +0,0 @@ -package checkpoint - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/spf13/cobra" -) - -type listOptions struct { - checkpointDir string -} - -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts listOptions - - cmd := &cobra.Command{ - Use: "ls [OPTIONS] CONTAINER", - Aliases: []string{"list"}, - Short: "List checkpoints for a container", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, args[0], opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory") - - return cmd - -} - -func runList(dockerCli *command.DockerCli, container string, opts listOptions) error { - client := dockerCli.Client() - - listOpts := types.CheckpointListOptions{ - CheckpointDir: opts.checkpointDir, - } - - checkpoints, err := client.CheckpointList(context.Background(), container, listOpts) - if err != nil { - return err - } - - cpCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewCheckpointFormat(formatter.TableFormatKey), - } - return formatter.CheckpointWrite(cpCtx, checkpoints) -} diff --git a/cli/command/checkpoint/remove.go b/cli/command/checkpoint/remove.go deleted file mode 100644 index ec39fa7b55..0000000000 --- a/cli/command/checkpoint/remove.go +++ /dev/null @@ -1,44 +0,0 @@ -package checkpoint - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -type removeOptions struct { - checkpointDir string -} - -func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts removeOptions - - cmd := &cobra.Command{ - Use: "rm [OPTIONS] CONTAINER CHECKPOINT", - Aliases: []string{"remove"}, - Short: "Remove a checkpoint", - Args: cli.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(dockerCli, args[0], args[1], opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory") - - return cmd -} - -func runRemove(dockerCli *command.DockerCli, container string, checkpoint string, opts removeOptions) error { - client := dockerCli.Client() - - removeOpts := types.CheckpointDeleteOptions{ - CheckpointID: checkpoint, - CheckpointDir: opts.checkpointDir, - } - - return client.CheckpointDelete(context.Background(), container, removeOpts) -} diff --git a/cli/command/cli.go b/cli/command/cli.go deleted file mode 100644 index 82940169ef..0000000000 --- a/cli/command/cli.go +++ /dev/null @@ -1,310 +0,0 @@ -package command - -import ( - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "runtime" - - "github.com/docker/docker/api" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/versions" - cliconfig "github.com/docker/docker/cli/config" - "github.com/docker/docker/cli/config/configfile" - "github.com/docker/docker/cli/config/credentials" - cliflags "github.com/docker/docker/cli/flags" - "github.com/docker/docker/client" - "github.com/docker/docker/dockerversion" - dopts "github.com/docker/docker/opts" - "github.com/docker/go-connections/sockets" - "github.com/docker/go-connections/tlsconfig" - "github.com/docker/notary/passphrase" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -// Streams is an interface which exposes the standard input and output streams -type Streams interface { - In() *InStream - Out() *OutStream - Err() io.Writer -} - -// Cli represents the docker command line client. -type Cli interface { - Client() client.APIClient - Out() *OutStream - Err() io.Writer - In() *InStream - SetIn(in *InStream) - ConfigFile() *configfile.ConfigFile - CredentialsStore(serverAddress string) credentials.Store -} - -// DockerCli is an instance the docker command line client. -// Instances of the client can be returned from NewDockerCli. -type DockerCli struct { - configFile *configfile.ConfigFile - in *InStream - out *OutStream - err io.Writer - keyFile string - client client.APIClient - defaultVersion string - server ServerInfo -} - -// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified. -func (cli *DockerCli) DefaultVersion() string { - return cli.defaultVersion -} - -// Client returns the APIClient -func (cli *DockerCli) Client() client.APIClient { - return cli.client -} - -// Out returns the writer used for stdout -func (cli *DockerCli) Out() *OutStream { - 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 *InStream) { - cli.in = in -} - -// In returns the reader used for stdin -func (cli *DockerCli) In() *InStream { - return cli.in -} - -// ShowHelp shows the command help. -func (cli *DockerCli) ShowHelp(cmd *cobra.Command, args []string) error { - cmd.SetOutput(cli.err) - cmd.HelpFunc()(cmd, args) - return nil -} - -// ConfigFile returns the ConfigFile -func (cli *DockerCli) ConfigFile() *configfile.ConfigFile { - return cli.configFile -} - -// ServerInfo returns the server version details for the host this client is -// connected to -func (cli *DockerCli) ServerInfo() ServerInfo { - return cli.server -} - -// GetAllCredentials returns all of the credentials stored in all of the -// configured credential stores. -func (cli *DockerCli) GetAllCredentials() (map[string]types.AuthConfig, error) { - auths := make(map[string]types.AuthConfig) - for registry := range cli.configFile.CredentialHelpers { - helper := cli.CredentialsStore(registry) - newAuths, err := helper.GetAll() - if err != nil { - return nil, err - } - addAll(auths, newAuths) - } - defaultStore := cli.CredentialsStore("") - newAuths, err := defaultStore.GetAll() - if err != nil { - return nil, err - } - addAll(auths, newAuths) - return auths, nil -} - -func addAll(to, from map[string]types.AuthConfig) { - for reg, ac := range from { - to[reg] = ac - } -} - -// CredentialsStore returns a new credentials store based -// on the settings provided in the configuration file. Empty string returns -// the default credential store. -func (cli *DockerCli) CredentialsStore(serverAddress string) credentials.Store { - if helper := getConfiguredCredentialStore(cli.configFile, serverAddress); helper != "" { - return credentials.NewNativeStore(cli.configFile, helper) - } - return credentials.NewFileStore(cli.configFile) -} - -// getConfiguredCredentialStore returns the credential helper configured for the -// given registry, the default credsStore, or the empty string if neither are -// configured. -func getConfiguredCredentialStore(c *configfile.ConfigFile, serverAddress string) string { - if c.CredentialHelpers != nil && serverAddress != "" { - if helper, exists := c.CredentialHelpers[serverAddress]; exists { - return helper - } - } - return c.CredentialsStore -} - -// Initialize the dockerCli runs initialization that must happen after command -// line flags are parsed. -func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { - cli.configFile = LoadDefaultConfigFile(cli.err) - - var err error - cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile) - if tlsconfig.IsErrEncryptedKey(err) { - var ( - passwd string - giveup bool - ) - passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil) - - for attempts := 0; tlsconfig.IsErrEncryptedKey(err); attempts++ { - // some code and comments borrowed from notary/trustmanager/keystore.go - passwd, giveup, err = passRetriever("private", "encrypted TLS private", false, attempts) - // Check if the passphrase retriever got an error or if it is telling us to give up - if giveup || err != nil { - return errors.Wrap(err, "private key is encrypted, but could not get passphrase") - } - - opts.Common.TLSOptions.Passphrase = passwd - cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile) - } - } - - if err != nil { - return err - } - - cli.defaultVersion = cli.client.ClientVersion() - - if opts.Common.TrustKey == "" { - cli.keyFile = filepath.Join(cliconfig.Dir(), cliflags.DefaultTrustKeyFile) - } else { - cli.keyFile = opts.Common.TrustKey - } - - if ping, err := cli.client.Ping(context.Background()); err == nil { - cli.server = ServerInfo{ - HasExperimental: ping.Experimental, - OSType: ping.OSType, - } - - // since the new header was added in 1.25, assume server is 1.24 if header is not present. - if ping.APIVersion == "" { - ping.APIVersion = "1.24" - } - - // if server version is lower than the current cli, downgrade - if versions.LessThan(ping.APIVersion, cli.client.ClientVersion()) { - cli.client.UpdateClientVersion(ping.APIVersion) - } - } - - return nil -} - -// ServerInfo stores details about the supported features and platform of the -// server -type ServerInfo struct { - HasExperimental bool - OSType string -} - -// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err. -func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli { - return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err} -} - -// LoadDefaultConfigFile attempts to load the default config file and returns -// an initialized ConfigFile struct if none is found. -func LoadDefaultConfigFile(err io.Writer) *configfile.ConfigFile { - configFile, e := cliconfig.Load(cliconfig.Dir()) - if e != nil { - fmt.Fprintf(err, "WARNING: Error loading config file:%v\n", e) - } - if !configFile.ContainsAuth() { - credentials.DetectDefaultStore(configFile) - } - return configFile -} - -// NewAPIClientFromFlags creates a new APIClient from command line flags -func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) { - host, err := getServerHost(opts.Hosts, opts.TLSOptions) - if err != nil { - return &client.Client{}, err - } - - customHeaders := configFile.HTTPHeaders - if customHeaders == nil { - customHeaders = map[string]string{} - } - customHeaders["User-Agent"] = UserAgent() - - verStr := api.DefaultVersion - if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" { - verStr = tmpStr - } - - httpClient, err := newHTTPClient(host, opts.TLSOptions) - if err != nil { - return &client.Client{}, err - } - - return client.NewClient(host, verStr, httpClient, customHeaders) -} - -func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (host string, err error) { - switch len(hosts) { - case 0: - host = os.Getenv("DOCKER_HOST") - case 1: - host = hosts[0] - default: - return "", errors.New("Please specify only one -H") - } - - host, err = dopts.ParseHost(tlsOptions != nil, host) - return -} - -func newHTTPClient(host string, tlsOptions *tlsconfig.Options) (*http.Client, error) { - if tlsOptions == nil { - // let the api client configure the default transport. - return nil, nil - } - opts := *tlsOptions - opts.ExclusiveRootPools = true - config, err := tlsconfig.Client(opts) - if err != nil { - return nil, err - } - tr := &http.Transport{ - TLSClientConfig: config, - } - proto, addr, _, err := client.ParseHost(host) - if err != nil { - return nil, err - } - - sockets.ConfigureTransport(tr, proto, addr) - - return &http.Client{ - Transport: tr, - }, nil -} - -// UserAgent returns the user agent string used for making API requests -func UserAgent() string { - return "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")" -} diff --git a/cli/command/commands/commands.go b/cli/command/commands/commands.go deleted file mode 100644 index 0db7f3a409..0000000000 --- a/cli/command/commands/commands.go +++ /dev/null @@ -1,121 +0,0 @@ -package commands - -import ( - "os" - - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/checkpoint" - "github.com/docker/docker/cli/command/container" - "github.com/docker/docker/cli/command/image" - "github.com/docker/docker/cli/command/network" - "github.com/docker/docker/cli/command/node" - "github.com/docker/docker/cli/command/plugin" - "github.com/docker/docker/cli/command/registry" - "github.com/docker/docker/cli/command/secret" - "github.com/docker/docker/cli/command/service" - "github.com/docker/docker/cli/command/stack" - "github.com/docker/docker/cli/command/swarm" - "github.com/docker/docker/cli/command/system" - "github.com/docker/docker/cli/command/volume" - "github.com/spf13/cobra" -) - -// AddCommands adds all the commands from cli/command to the root command -func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { - cmd.AddCommand( - // checkpoint - checkpoint.NewCheckpointCommand(dockerCli), - - // container - container.NewContainerCommand(dockerCli), - container.NewRunCommand(dockerCli), - - // image - image.NewImageCommand(dockerCli), - image.NewBuildCommand(dockerCli), - - // node - node.NewNodeCommand(dockerCli), - - // network - network.NewNetworkCommand(dockerCli), - - // plugin - plugin.NewPluginCommand(dockerCli), - - // registry - registry.NewLoginCommand(dockerCli), - registry.NewLogoutCommand(dockerCli), - registry.NewSearchCommand(dockerCli), - - // secret - secret.NewSecretCommand(dockerCli), - - // service - service.NewServiceCommand(dockerCli), - - // system - system.NewSystemCommand(dockerCli), - system.NewVersionCommand(dockerCli), - - // stack - stack.NewStackCommand(dockerCli), - stack.NewTopLevelDeployCommand(dockerCli), - - // swarm - swarm.NewSwarmCommand(dockerCli), - - // volume - volume.NewVolumeCommand(dockerCli), - - // legacy commands may be hidden - hide(system.NewEventsCommand(dockerCli)), - hide(system.NewInfoCommand(dockerCli)), - hide(system.NewInspectCommand(dockerCli)), - hide(container.NewAttachCommand(dockerCli)), - hide(container.NewCommitCommand(dockerCli)), - hide(container.NewCopyCommand(dockerCli)), - hide(container.NewCreateCommand(dockerCli)), - hide(container.NewDiffCommand(dockerCli)), - hide(container.NewExecCommand(dockerCli)), - hide(container.NewExportCommand(dockerCli)), - hide(container.NewKillCommand(dockerCli)), - hide(container.NewLogsCommand(dockerCli)), - hide(container.NewPauseCommand(dockerCli)), - hide(container.NewPortCommand(dockerCli)), - hide(container.NewPsCommand(dockerCli)), - hide(container.NewRenameCommand(dockerCli)), - hide(container.NewRestartCommand(dockerCli)), - hide(container.NewRmCommand(dockerCli)), - hide(container.NewStartCommand(dockerCli)), - hide(container.NewStatsCommand(dockerCli)), - hide(container.NewStopCommand(dockerCli)), - hide(container.NewTopCommand(dockerCli)), - hide(container.NewUnpauseCommand(dockerCli)), - hide(container.NewUpdateCommand(dockerCli)), - hide(container.NewWaitCommand(dockerCli)), - hide(image.NewHistoryCommand(dockerCli)), - hide(image.NewImagesCommand(dockerCli)), - hide(image.NewImportCommand(dockerCli)), - hide(image.NewLoadCommand(dockerCli)), - hide(image.NewPullCommand(dockerCli)), - hide(image.NewPushCommand(dockerCli)), - hide(image.NewRemoveCommand(dockerCli)), - hide(image.NewSaveCommand(dockerCli)), - hide(image.NewTagCommand(dockerCli)), - ) - -} - -func hide(cmd *cobra.Command) *cobra.Command { - // If the environment variable with name "DOCKER_HIDE_LEGACY_COMMANDS" is not empty, - // these legacy commands (such as `docker ps`, `docker exec`, etc) - // will not be shown in output console. - if os.Getenv("DOCKER_HIDE_LEGACY_COMMANDS") == "" { - return cmd - } - cmdCopy := *cmd - cmdCopy.Hidden = true - cmdCopy.Aliases = []string{} - return &cmdCopy -} diff --git a/cli/command/container/attach.go b/cli/command/container/attach.go deleted file mode 100644 index 0564bdcd0f..0000000000 --- a/cli/command/container/attach.go +++ /dev/null @@ -1,129 +0,0 @@ -package container - -import ( - "io" - "net/http/httputil" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/signal" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type attachOptions struct { - noStdin bool - proxy bool - detachKeys string - - container string -} - -// NewAttachCommand creates a new cobra.Command for `docker attach` -func NewAttachCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts attachOptions - - cmd := &cobra.Command{ - Use: "attach [OPTIONS] CONTAINER", - Short: "Attach local standard input, output, and error streams to a running container", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.container = args[0] - return runAttach(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.BoolVar(&opts.noStdin, "no-stdin", false, "Do not attach STDIN") - flags.BoolVar(&opts.proxy, "sig-proxy", true, "Proxy all received signals to the process") - flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container") - return cmd -} - -func runAttach(dockerCli *command.DockerCli, opts *attachOptions) error { - ctx := context.Background() - client := dockerCli.Client() - - c, err := client.ContainerInspect(ctx, opts.container) - if err != nil { - return err - } - - if !c.State.Running { - return errors.New("You cannot attach to a stopped container, start it first") - } - - if c.State.Paused { - return errors.New("You cannot attach to a paused container, unpause it first") - } - - if err := dockerCli.In().CheckTty(!opts.noStdin, c.Config.Tty); err != nil { - return err - } - - if opts.detachKeys != "" { - dockerCli.ConfigFile().DetachKeys = opts.detachKeys - } - - options := types.ContainerAttachOptions{ - Stream: true, - Stdin: !opts.noStdin && c.Config.OpenStdin, - Stdout: true, - Stderr: true, - DetachKeys: dockerCli.ConfigFile().DetachKeys, - } - - var in io.ReadCloser - if options.Stdin { - in = dockerCli.In() - } - - if opts.proxy && !c.Config.Tty { - sigc := ForwardAllSignals(ctx, dockerCli, opts.container) - defer signal.StopCatch(sigc) - } - - resp, errAttach := client.ContainerAttach(ctx, opts.container, options) - if errAttach != nil && errAttach != httputil.ErrPersistEOF { - // ContainerAttach returns an ErrPersistEOF (connection closed) - // means server met an error and put it in Hijacked connection - // keep the error and read detailed error message from hijacked connection later - return errAttach - } - defer resp.Close() - - if c.Config.Tty && dockerCli.Out().IsTerminal() { - height, width := dockerCli.Out().GetTtySize() - // To handle the case where a user repeatedly attaches/detaches without resizing their - // terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially - // resize it, then go back to normal. Without this, every attach after the first will - // require the user to manually resize or hit enter. - resizeTtyTo(ctx, client, opts.container, height+1, width+1, false) - - // After the above resizing occurs, the call to MonitorTtySize below will handle resetting back - // to the actual size. - if err := MonitorTtySize(ctx, dockerCli, opts.container, false); err != nil { - logrus.Debugf("Error monitoring TTY size: %s", err) - } - } - if err := holdHijackedConnection(ctx, dockerCli, c.Config.Tty, in, dockerCli.Out(), dockerCli.Err(), resp); err != nil { - return err - } - - if errAttach != nil { - return errAttach - } - - _, status, err := getExitCode(ctx, dockerCli, opts.container) - if err != nil { - return err - } - if status != 0 { - return cli.StatusError{StatusCode: status} - } - - return nil -} diff --git a/cli/command/container/cmd.go b/cli/command/container/cmd.go deleted file mode 100644 index b78411e0a3..0000000000 --- a/cli/command/container/cmd.go +++ /dev/null @@ -1,45 +0,0 @@ -package container - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -// NewContainerCommand returns a cobra command for `container` subcommands -func NewContainerCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "container", - Short: "Manage containers", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - } - cmd.AddCommand( - NewAttachCommand(dockerCli), - NewCommitCommand(dockerCli), - NewCopyCommand(dockerCli), - NewCreateCommand(dockerCli), - NewDiffCommand(dockerCli), - NewExecCommand(dockerCli), - NewExportCommand(dockerCli), - NewKillCommand(dockerCli), - NewLogsCommand(dockerCli), - NewPauseCommand(dockerCli), - NewPortCommand(dockerCli), - NewRenameCommand(dockerCli), - NewRestartCommand(dockerCli), - NewRmCommand(dockerCli), - NewRunCommand(dockerCli), - NewStartCommand(dockerCli), - NewStatsCommand(dockerCli), - NewStopCommand(dockerCli), - NewTopCommand(dockerCli), - NewUnpauseCommand(dockerCli), - NewUpdateCommand(dockerCli), - NewWaitCommand(dockerCli), - newListCommand(dockerCli), - newInspectCommand(dockerCli), - NewPruneCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/container/commit.go b/cli/command/container/commit.go deleted file mode 100644 index 8f67d96d87..0000000000 --- a/cli/command/container/commit.go +++ /dev/null @@ -1,75 +0,0 @@ -package container - -import ( - "fmt" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - dockeropts "github.com/docker/docker/opts" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type commitOptions struct { - container string - reference string - - pause bool - comment string - author string - changes dockeropts.ListOpts -} - -// NewCommitCommand creates a new cobra.Command for `docker commit` -func NewCommitCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts commitOptions - - cmd := &cobra.Command{ - Use: "commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]", - Short: "Create a new image from a container's changes", - Args: cli.RequiresRangeArgs(1, 2), - RunE: func(cmd *cobra.Command, args []string) error { - opts.container = args[0] - if len(args) > 1 { - opts.reference = args[1] - } - return runCommit(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.SetInterspersed(false) - - flags.BoolVarP(&opts.pause, "pause", "p", true, "Pause container during commit") - flags.StringVarP(&opts.comment, "message", "m", "", "Commit message") - flags.StringVarP(&opts.author, "author", "a", "", "Author (e.g., \"John Hannibal Smith \")") - - opts.changes = dockeropts.NewListOpts(nil) - flags.VarP(&opts.changes, "change", "c", "Apply Dockerfile instruction to the created image") - - return cmd -} - -func runCommit(dockerCli *command.DockerCli, opts *commitOptions) error { - ctx := context.Background() - - name := opts.container - reference := opts.reference - - options := types.ContainerCommitOptions{ - Reference: reference, - Comment: opts.comment, - Author: opts.author, - Changes: opts.changes.GetAll(), - Pause: opts.pause, - } - - response, err := dockerCli.Client().ContainerCommit(ctx, name, options) - if err != nil { - return err - } - - fmt.Fprintln(dockerCli.Out(), response.ID) - return nil -} diff --git a/cli/command/container/cp.go b/cli/command/container/cp.go deleted file mode 100644 index a4165a18d2..0000000000 --- a/cli/command/container/cp.go +++ /dev/null @@ -1,305 +0,0 @@ -package container - -import ( - "io" - "os" - "path/filepath" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/archive" - "github.com/docker/docker/pkg/system" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type copyOptions struct { - source string - destination string - followLink bool - copyUIDGID bool -} - -type copyDirection int - -const ( - fromContainer copyDirection = (1 << iota) - toContainer - acrossContainers = fromContainer | toContainer -) - -type cpConfig struct { - followLink bool -} - -// NewCopyCommand creates a new `docker cp` command -func NewCopyCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts copyOptions - - cmd := &cobra.Command{ - Use: `cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|- - docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH`, - Short: "Copy files/folders between a container and the local filesystem", - Long: strings.Join([]string{ - "Copy files/folders between a container and the local filesystem\n", - "\nUse '-' as the source to read a tar archive from stdin\n", - "and extract it to a directory destination in a container.\n", - "Use '-' as the destination to stream a tar archive of a\n", - "container source to stdout.", - }, ""), - Args: cli.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - if args[0] == "" { - return errors.New("source can not be empty") - } - if args[1] == "" { - return errors.New("destination can not be empty") - } - opts.source = args[0] - opts.destination = args[1] - return runCopy(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH") - flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)") - - return cmd -} - -func runCopy(dockerCli *command.DockerCli, opts copyOptions) error { - srcContainer, srcPath := splitCpArg(opts.source) - dstContainer, dstPath := splitCpArg(opts.destination) - - var direction copyDirection - if srcContainer != "" { - direction |= fromContainer - } - if dstContainer != "" { - direction |= toContainer - } - - cpParam := &cpConfig{ - followLink: opts.followLink, - } - - ctx := context.Background() - - switch direction { - case fromContainer: - return copyFromContainer(ctx, dockerCli, srcContainer, srcPath, dstPath, cpParam) - case toContainer: - return copyToContainer(ctx, dockerCli, srcPath, dstContainer, dstPath, cpParam, opts.copyUIDGID) - case acrossContainers: - // Copying between containers isn't supported. - return errors.New("copying between containers is not supported") - default: - // User didn't specify any container. - return errors.New("must specify at least one container source") - } -} - -func statContainerPath(ctx context.Context, dockerCli *command.DockerCli, containerName, path string) (types.ContainerPathStat, error) { - return dockerCli.Client().ContainerStatPath(ctx, containerName, path) -} - -func resolveLocalPath(localPath string) (absPath string, err error) { - if absPath, err = filepath.Abs(localPath); err != nil { - return - } - - return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil -} - -func copyFromContainer(ctx context.Context, dockerCli *command.DockerCli, srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) { - if dstPath != "-" { - // Get an absolute destination path. - dstPath, err = resolveLocalPath(dstPath) - if err != nil { - return err - } - } - - // if client requests to follow symbol link, then must decide target file to be copied - var rebaseName string - if cpParam.followLink { - srcStat, err := statContainerPath(ctx, dockerCli, srcContainer, srcPath) - - // If the destination is a symbolic link, we should follow it. - if err == nil && srcStat.Mode&os.ModeSymlink != 0 { - linkTarget := srcStat.LinkTarget - if !system.IsAbs(linkTarget) { - // Join with the parent directory. - srcParent, _ := archive.SplitPathDirEntry(srcPath) - linkTarget = filepath.Join(srcParent, linkTarget) - } - - linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget) - srcPath = linkTarget - } - - } - - content, stat, err := dockerCli.Client().CopyFromContainer(ctx, srcContainer, srcPath) - if err != nil { - return err - } - defer content.Close() - - if dstPath == "-" { - // Send the response to STDOUT. - _, err = io.Copy(os.Stdout, content) - - return err - } - - // Prepare source copy info. - srcInfo := archive.CopyInfo{ - Path: srcPath, - Exists: true, - IsDir: stat.Mode.IsDir(), - RebaseName: rebaseName, - } - - preArchive := content - if len(srcInfo.RebaseName) != 0 { - _, srcBase := archive.SplitPathDirEntry(srcInfo.Path) - preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName) - } - // See comments in the implementation of `archive.CopyTo` for exactly what - // goes into deciding how and whether the source archive needs to be - // altered for the correct copy behavior. - return archive.CopyTo(preArchive, srcInfo, dstPath) -} - -func copyToContainer(ctx context.Context, dockerCli *command.DockerCli, srcPath, dstContainer, dstPath string, cpParam *cpConfig, copyUIDGID bool) (err error) { - if srcPath != "-" { - // Get an absolute source path. - srcPath, err = resolveLocalPath(srcPath) - if err != nil { - return err - } - } - - // In order to get the copy behavior right, we need to know information - // about both the source and destination. The API is a simple tar - // archive/extract API but we can use the stat info header about the - // destination to be more informed about exactly what the destination is. - - // Prepare destination copy info by stat-ing the container path. - dstInfo := archive.CopyInfo{Path: dstPath} - dstStat, err := statContainerPath(ctx, dockerCli, dstContainer, dstPath) - - // If the destination is a symbolic link, we should evaluate it. - if err == nil && dstStat.Mode&os.ModeSymlink != 0 { - linkTarget := dstStat.LinkTarget - if !system.IsAbs(linkTarget) { - // Join with the parent directory. - dstParent, _ := archive.SplitPathDirEntry(dstPath) - linkTarget = filepath.Join(dstParent, linkTarget) - } - - dstInfo.Path = linkTarget - dstStat, err = statContainerPath(ctx, dockerCli, dstContainer, linkTarget) - } - - // Ignore any error and assume that the parent directory of the destination - // path exists, in which case the copy may still succeed. If there is any - // type of conflict (e.g., non-directory overwriting an existing directory - // or vice versa) the extraction will fail. If the destination simply did - // not exist, but the parent directory does, the extraction will still - // succeed. - if err == nil { - dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir() - } - - var ( - content io.Reader - resolvedDstPath string - ) - - if srcPath == "-" { - // Use STDIN. - content = os.Stdin - resolvedDstPath = dstInfo.Path - if !dstInfo.IsDir { - return errors.Errorf("destination \"%s:%s\" must be a directory", dstContainer, dstPath) - } - } else { - // Prepare source copy info. - srcInfo, err := archive.CopyInfoSourcePath(srcPath, cpParam.followLink) - if err != nil { - return err - } - - srcArchive, err := archive.TarResource(srcInfo) - if err != nil { - return err - } - defer srcArchive.Close() - - // With the stat info about the local source as well as the - // destination, we have enough information to know whether we need to - // alter the archive that we upload so that when the server extracts - // it to the specified directory in the container we get the desired - // copy behavior. - - // See comments in the implementation of `archive.PrepareArchiveCopy` - // for exactly what goes into deciding how and whether the source - // archive needs to be altered for the correct copy behavior when it is - // extracted. This function also infers from the source and destination - // info which directory to extract to, which may be the parent of the - // destination that the user specified. - dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo) - if err != nil { - return err - } - defer preparedArchive.Close() - - resolvedDstPath = dstDir - content = preparedArchive - } - - options := types.CopyToContainerOptions{ - AllowOverwriteDirWithFile: false, - CopyUIDGID: copyUIDGID, - } - - return dockerCli.Client().CopyToContainer(ctx, dstContainer, resolvedDstPath, content, options) -} - -// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be -// in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by -// requiring a LOCALPATH with a `:` to be made explicit with a relative or -// absolute path: -// `/path/to/file:name.txt` or `./file:name.txt` -// -// This is apparently how `scp` handles this as well: -// http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/ -// -// We can't simply check for a filepath separator because container names may -// have a separator, e.g., "host0/cname1" if container is in a Docker cluster, -// so we have to check for a `/` or `.` prefix. Also, in the case of a Windows -// client, a `:` could be part of an absolute Windows path, in which case it -// is immediately proceeded by a backslash. -func splitCpArg(arg string) (container, path string) { - if system.IsAbs(arg) { - // Explicit local absolute path, e.g., `C:\foo` or `/foo`. - return "", arg - } - - parts := strings.SplitN(arg, ":", 2) - - if len(parts) == 1 || strings.HasPrefix(parts[0], ".") { - // Either there's no `:` in the arg - // OR it's an explicit local relative path like `./file:name.txt`. - return "", arg - } - - return parts[0], parts[1] -} diff --git a/cli/command/container/create.go b/cli/command/container/create.go deleted file mode 100644 index 9222b4060b..0000000000 --- a/cli/command/container/create.go +++ /dev/null @@ -1,224 +0,0 @@ -package container - -import ( - "fmt" - "io" - "os" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/image" - apiclient "github.com/docker/docker/client" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/registry" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "golang.org/x/net/context" -) - -type createOptions struct { - name string -} - -// NewCreateCommand creates a new cobra.Command for `docker create` -func NewCreateCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts createOptions - var copts *containerOptions - - cmd := &cobra.Command{ - Use: "create [OPTIONS] IMAGE [COMMAND] [ARG...]", - Short: "Create a new container", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - copts.Image = args[0] - if len(args) > 1 { - copts.Args = args[1:] - } - return runCreate(dockerCli, cmd.Flags(), &opts, copts) - }, - } - - flags := cmd.Flags() - flags.SetInterspersed(false) - - flags.StringVar(&opts.name, "name", "", "Assign a name to the container") - - // Add an explicit help that doesn't have a `-h` to prevent the conflict - // with hostname - flags.Bool("help", false, "Print usage") - - command.AddTrustVerificationFlags(flags) - copts = addFlags(flags) - return cmd -} - -func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *createOptions, copts *containerOptions) error { - containerConfig, err := parse(flags, copts) - if err != nil { - reportError(dockerCli.Err(), "create", err.Error(), true) - return cli.StatusError{StatusCode: 125} - } - response, err := createContainer(context.Background(), dockerCli, containerConfig, opts.name) - if err != nil { - return err - } - fmt.Fprintln(dockerCli.Out(), response.ID) - return nil -} - -func pullImage(ctx context.Context, dockerCli *command.DockerCli, image string, out io.Writer) error { - ref, err := reference.ParseNormalizedNamed(image) - if err != nil { - return err - } - - // Resolve the Repository name from fqn to RepositoryInfo - repoInfo, err := registry.ParseRepositoryInfo(ref) - if err != nil { - return err - } - - authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) - encodedAuth, err := command.EncodeAuthToBase64(authConfig) - if err != nil { - return err - } - - options := types.ImageCreateOptions{ - RegistryAuth: encodedAuth, - } - - responseBody, err := dockerCli.Client().ImageCreate(ctx, image, options) - if err != nil { - return err - } - defer responseBody.Close() - - return jsonmessage.DisplayJSONMessagesStream( - responseBody, - out, - dockerCli.Out().FD(), - dockerCli.Out().IsTerminal(), - nil) -} - -type cidFile struct { - path string - file *os.File - written bool -} - -func (cid *cidFile) Close() error { - cid.file.Close() - - if cid.written { - return nil - } - if err := os.Remove(cid.path); err != nil { - return errors.Errorf("failed to remove the CID file '%s': %s \n", cid.path, err) - } - - return nil -} - -func (cid *cidFile) Write(id string) error { - if _, err := cid.file.Write([]byte(id)); err != nil { - return errors.Errorf("Failed to write the container ID to the file: %s", err) - } - cid.written = true - return nil -} - -func newCIDFile(path string) (*cidFile, error) { - if _, err := os.Stat(path); err == nil { - return nil, errors.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path) - } - - f, err := os.Create(path) - if err != nil { - return nil, errors.Errorf("Failed to create the container ID file: %s", err) - } - - return &cidFile{path: path, file: f}, nil -} - -func createContainer(ctx context.Context, dockerCli *command.DockerCli, containerConfig *containerConfig, name string) (*container.ContainerCreateCreatedBody, error) { - config := containerConfig.Config - hostConfig := containerConfig.HostConfig - networkingConfig := containerConfig.NetworkingConfig - stderr := dockerCli.Err() - - var ( - containerIDFile *cidFile - trustedRef reference.Canonical - namedRef reference.Named - ) - - cidfile := hostConfig.ContainerIDFile - if cidfile != "" { - var err error - if containerIDFile, err = newCIDFile(cidfile); err != nil { - return nil, err - } - defer containerIDFile.Close() - } - - ref, err := reference.ParseAnyReference(config.Image) - if err != nil { - return nil, err - } - if named, ok := ref.(reference.Named); ok { - namedRef = reference.TagNameOnly(named) - - if taggedRef, ok := namedRef.(reference.NamedTagged); ok && command.IsTrusted() { - var err error - trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef, nil) - if err != nil { - return nil, err - } - config.Image = reference.FamiliarString(trustedRef) - } - } - - //create the container - response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, name) - - //if image not found try to pull it - if err != nil { - if apiclient.IsErrImageNotFound(err) && namedRef != nil { - fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef)) - - // we don't want to write to stdout anything apart from container.ID - if err = pullImage(ctx, dockerCli, config.Image, stderr); err != nil { - return nil, err - } - if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil { - if err := image.TagTrusted(ctx, dockerCli, trustedRef, taggedRef); err != nil { - return nil, err - } - } - // Retry - var retryErr error - response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, name) - if retryErr != nil { - return nil, retryErr - } - } else { - return nil, err - } - } - - for _, warning := range response.Warnings { - fmt.Fprintf(stderr, "WARNING: %s\n", warning) - } - if containerIDFile != nil { - if err = containerIDFile.Write(response.ID); err != nil { - return nil, err - } - } - return &response, nil -} diff --git a/cli/command/container/diff.go b/cli/command/container/diff.go deleted file mode 100644 index 816a0a56a3..0000000000 --- a/cli/command/container/diff.go +++ /dev/null @@ -1,46 +0,0 @@ -package container - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type diffOptions struct { - container string -} - -// NewDiffCommand creates a new cobra.Command for `docker diff` -func NewDiffCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts diffOptions - - return &cobra.Command{ - Use: "diff CONTAINER", - Short: "Inspect changes to files or directories on a container's filesystem", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.container = args[0] - return runDiff(dockerCli, &opts) - }, - } -} - -func runDiff(dockerCli *command.DockerCli, opts *diffOptions) error { - if opts.container == "" { - return errors.New("Container name cannot be empty") - } - ctx := context.Background() - - changes, err := dockerCli.Client().ContainerDiff(ctx, opts.container) - if err != nil { - return err - } - diffCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewDiffFormat("{{.Type}} {{.Path}}"), - } - return formatter.DiffWrite(diffCtx, changes) -} diff --git a/cli/command/container/exec.go b/cli/command/container/exec.go deleted file mode 100644 index 676708c77b..0000000000 --- a/cli/command/container/exec.go +++ /dev/null @@ -1,205 +0,0 @@ -package container - -import ( - "fmt" - "io" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - apiclient "github.com/docker/docker/client" - options "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/promise" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type execOptions struct { - detachKeys string - interactive bool - tty bool - detach bool - user string - privileged bool - env *options.ListOpts -} - -func newExecOptions() *execOptions { - var values []string - return &execOptions{ - env: options.NewListOptsRef(&values, options.ValidateEnv), - } -} - -// NewExecCommand creates a new cobra.Command for `docker exec` -func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := newExecOptions() - - cmd := &cobra.Command{ - Use: "exec [OPTIONS] CONTAINER COMMAND [ARG...]", - Short: "Run a command in a running container", - Args: cli.RequiresMinArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - container := args[0] - execCmd := args[1:] - return runExec(dockerCli, opts, container, execCmd) - }, - } - - flags := cmd.Flags() - flags.SetInterspersed(false) - - flags.StringVarP(&opts.detachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container") - flags.BoolVarP(&opts.interactive, "interactive", "i", false, "Keep STDIN open even if not attached") - flags.BoolVarP(&opts.tty, "tty", "t", false, "Allocate a pseudo-TTY") - flags.BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: run command in the background") - flags.StringVarP(&opts.user, "user", "u", "", "Username or UID (format: [:])") - flags.BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the command") - flags.VarP(opts.env, "env", "e", "Set environment variables") - flags.SetAnnotation("env", "version", []string{"1.25"}) - - return cmd -} - -func runExec(dockerCli *command.DockerCli, opts *execOptions, container string, execCmd []string) error { - execConfig, err := parseExec(opts, execCmd) - // just in case the ParseExec does not exit - if container == "" || err != nil { - return cli.StatusError{StatusCode: 1} - } - - if opts.detachKeys != "" { - dockerCli.ConfigFile().DetachKeys = opts.detachKeys - } - - // Send client escape keys - execConfig.DetachKeys = dockerCli.ConfigFile().DetachKeys - - ctx := context.Background() - client := dockerCli.Client() - - response, err := client.ContainerExecCreate(ctx, container, *execConfig) - if err != nil { - return err - } - - execID := response.ID - if execID == "" { - fmt.Fprintln(dockerCli.Out(), "exec ID empty") - return nil - } - - //Temp struct for execStart so that we don't need to transfer all the execConfig - if !execConfig.Detach { - if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil { - return err - } - } else { - execStartCheck := types.ExecStartCheck{ - Detach: execConfig.Detach, - Tty: execConfig.Tty, - } - - if err := client.ContainerExecStart(ctx, execID, execStartCheck); err != nil { - return err - } - // For now don't print this - wait for when we support exec wait() - // fmt.Fprintf(dockerCli.Out(), "%s\n", execID) - return nil - } - - // Interactive exec requested. - var ( - out, stderr io.Writer - in io.ReadCloser - errCh chan error - ) - - if execConfig.AttachStdin { - in = dockerCli.In() - } - if execConfig.AttachStdout { - out = dockerCli.Out() - } - if execConfig.AttachStderr { - if execConfig.Tty { - stderr = dockerCli.Out() - } else { - stderr = dockerCli.Err() - } - } - - resp, err := client.ContainerExecAttach(ctx, execID, *execConfig) - if err != nil { - return err - } - defer resp.Close() - errCh = promise.Go(func() error { - return holdHijackedConnection(ctx, dockerCli, execConfig.Tty, in, out, stderr, resp) - }) - - if execConfig.Tty && dockerCli.In().IsTerminal() { - if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil { - fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err) - } - } - - if err := <-errCh; err != nil { - logrus.Debugf("Error hijack: %s", err) - return err - } - - var status int - if _, status, err = getExecExitCode(ctx, client, execID); err != nil { - return err - } - - if status != 0 { - return cli.StatusError{StatusCode: status} - } - - return nil -} - -// getExecExitCode perform an inspect on the exec command. It returns -// the running state and the exit code. -func getExecExitCode(ctx context.Context, client apiclient.ContainerAPIClient, execID string) (bool, int, error) { - resp, err := client.ContainerExecInspect(ctx, execID) - if err != nil { - // If we can't connect, then the daemon probably died. - if !apiclient.IsErrConnectionFailed(err) { - return false, -1, err - } - return false, -1, nil - } - - return resp.Running, resp.ExitCode, nil -} - -// parseExec parses the specified args for the specified command and generates -// an ExecConfig from it. -func parseExec(opts *execOptions, execCmd []string) (*types.ExecConfig, error) { - execConfig := &types.ExecConfig{ - User: opts.user, - Privileged: opts.privileged, - Tty: opts.tty, - Cmd: execCmd, - Detach: opts.detach, - } - - // If -d is not set, attach to everything by default - if !opts.detach { - execConfig.AttachStdout = true - execConfig.AttachStderr = true - if opts.interactive { - execConfig.AttachStdin = true - } - } - - if opts.env != nil { - execConfig.Env = opts.env.GetAll() - } - - return execConfig, nil -} diff --git a/cli/command/container/exec_test.go b/cli/command/container/exec_test.go deleted file mode 100644 index e94beb4545..0000000000 --- a/cli/command/container/exec_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package container - -import ( - "testing" - - "github.com/docker/docker/api/types" -) - -type arguments struct { - options execOptions - execCmd []string -} - -func TestParseExec(t *testing.T) { - valids := map[*arguments]*types.ExecConfig{ - { - execCmd: []string{"command"}, - }: { - Cmd: []string{"command"}, - AttachStdout: true, - AttachStderr: true, - }, - { - execCmd: []string{"command1", "command2"}, - }: { - Cmd: []string{"command1", "command2"}, - AttachStdout: true, - AttachStderr: true, - }, - { - options: execOptions{ - interactive: true, - tty: true, - user: "uid", - }, - execCmd: []string{"command"}, - }: { - User: "uid", - AttachStdin: true, - AttachStdout: true, - AttachStderr: true, - Tty: true, - Cmd: []string{"command"}, - }, - { - options: execOptions{ - detach: true, - }, - execCmd: []string{"command"}, - }: { - AttachStdin: false, - AttachStdout: false, - AttachStderr: false, - Detach: true, - Cmd: []string{"command"}, - }, - { - options: execOptions{ - tty: true, - interactive: true, - detach: true, - }, - execCmd: []string{"command"}, - }: { - AttachStdin: false, - AttachStdout: false, - AttachStderr: false, - Detach: true, - Tty: true, - Cmd: []string{"command"}, - }, - } - - for valid, expectedExecConfig := range valids { - execConfig, err := parseExec(&valid.options, valid.execCmd) - if err != nil { - t.Fatal(err) - } - if !compareExecConfig(expectedExecConfig, execConfig) { - t.Fatalf("Expected [%v] for %v, got [%v]", expectedExecConfig, valid, execConfig) - } - } -} - -func compareExecConfig(config1 *types.ExecConfig, config2 *types.ExecConfig) bool { - if config1.AttachStderr != config2.AttachStderr { - return false - } - if config1.AttachStdin != config2.AttachStdin { - return false - } - if config1.AttachStdout != config2.AttachStdout { - return false - } - if config1.Detach != config2.Detach { - return false - } - if config1.Privileged != config2.Privileged { - return false - } - if config1.Tty != config2.Tty { - return false - } - if config1.User != config2.User { - return false - } - if len(config1.Cmd) != len(config2.Cmd) { - return false - } - for index, value := range config1.Cmd { - if value != config2.Cmd[index] { - return false - } - } - return true -} diff --git a/cli/command/container/export.go b/cli/command/container/export.go deleted file mode 100644 index cb0ddfe7a7..0000000000 --- a/cli/command/container/export.go +++ /dev/null @@ -1,58 +0,0 @@ -package container - -import ( - "io" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type exportOptions struct { - container string - output string -} - -// NewExportCommand creates a new `docker export` command -func NewExportCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts exportOptions - - cmd := &cobra.Command{ - Use: "export [OPTIONS] CONTAINER", - Short: "Export a container's filesystem as a tar archive", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.container = args[0] - return runExport(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT") - - return cmd -} - -func runExport(dockerCli *command.DockerCli, opts exportOptions) error { - if opts.output == "" && dockerCli.Out().IsTerminal() { - return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.") - } - - clnt := dockerCli.Client() - - responseBody, err := clnt.ContainerExport(context.Background(), opts.container) - if err != nil { - return err - } - defer responseBody.Close() - - if opts.output == "" { - _, err := io.Copy(dockerCli.Out(), responseBody) - return err - } - - return command.CopyToFile(opts.output, responseBody) -} diff --git a/cli/command/container/hijack.go b/cli/command/container/hijack.go deleted file mode 100644 index 11acf114f0..0000000000 --- a/cli/command/container/hijack.go +++ /dev/null @@ -1,124 +0,0 @@ -package container - -import ( - "io" - "runtime" - "sync" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/stdcopy" - "golang.org/x/net/context" -) - -// holdHijackedConnection handles copying input to and output from streams to the -// connection -func holdHijackedConnection(ctx context.Context, streams command.Streams, tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error { - var ( - err error - restoreOnce sync.Once - ) - if inputStream != nil && tty { - if err := setRawTerminal(streams); err != nil { - return err - } - defer func() { - restoreOnce.Do(func() { - restoreTerminal(streams, inputStream) - }) - }() - } - - receiveStdout := make(chan error, 1) - if outputStream != nil || errorStream != nil { - go func() { - // When TTY is ON, use regular copy - if tty && outputStream != nil { - _, err = io.Copy(outputStream, resp.Reader) - // we should restore the terminal as soon as possible once connection end - // so any following print messages will be in normal type. - if inputStream != nil { - restoreOnce.Do(func() { - restoreTerminal(streams, inputStream) - }) - } - } else { - _, err = stdcopy.StdCopy(outputStream, errorStream, resp.Reader) - } - - logrus.Debug("[hijack] End of stdout") - receiveStdout <- err - }() - } - - stdinDone := make(chan struct{}) - go func() { - if inputStream != nil { - io.Copy(resp.Conn, inputStream) - // we should restore the terminal as soon as possible once connection end - // so any following print messages will be in normal type. - if tty { - restoreOnce.Do(func() { - restoreTerminal(streams, inputStream) - }) - } - logrus.Debug("[hijack] End of stdin") - } - - if err := resp.CloseWrite(); err != nil { - logrus.Debugf("Couldn't send EOF: %s", err) - } - close(stdinDone) - }() - - select { - case err := <-receiveStdout: - if err != nil { - logrus.Debugf("Error receiveStdout: %s", err) - return err - } - case <-stdinDone: - if outputStream != nil || errorStream != nil { - select { - case err := <-receiveStdout: - if err != nil { - logrus.Debugf("Error receiveStdout: %s", err) - return err - } - case <-ctx.Done(): - } - } - case <-ctx.Done(): - } - - return nil -} - -func setRawTerminal(streams command.Streams) error { - if err := streams.In().SetRawTerminal(); err != nil { - return err - } - return streams.Out().SetRawTerminal() -} - -func restoreTerminal(streams command.Streams, in io.Closer) error { - streams.In().RestoreTerminal() - streams.Out().RestoreTerminal() - // WARNING: DO NOT REMOVE THE OS CHECKS !!! - // For some reason this Close call blocks on darwin.. - // As the client exits right after, simply discard the close - // until we find a better solution. - // - // This can also cause the client on Windows to get stuck in Win32 CloseHandle() - // in some cases. See https://github.com/docker/docker/issues/28267#issuecomment-288237442 - // Tracked internally at Microsoft by VSO #11352156. In the - // Windows case, you hit this if you are using the native/v2 console, - // not the "legacy" console, and you start the client in a new window. eg - // `start docker run --rm -it microsoft/nanoserver cmd /s /c echo foobar` - // will hang. Remove start, and it won't repro. - if in != nil && runtime.GOOS != "darwin" && runtime.GOOS != "windows" { - return in.Close() - } - return nil -} diff --git a/cli/command/container/inspect.go b/cli/command/container/inspect.go deleted file mode 100644 index d08b38dc96..0000000000 --- a/cli/command/container/inspect.go +++ /dev/null @@ -1,46 +0,0 @@ -package container - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type inspectOptions struct { - format string - size bool - refs []string -} - -// newInspectCommand creates a new cobra.Command for `docker container inspect` -func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts inspectOptions - - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] CONTAINER [CONTAINER...]", - Short: "Display detailed information on one or more containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.refs = args - return runInspect(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes") - - return cmd -} - -func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - getRefFunc := func(ref string) (interface{}, []byte, error) { - return client.ContainerInspectWithRaw(ctx, ref, opts.size) - } - return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRefFunc) -} diff --git a/cli/command/container/kill.go b/cli/command/container/kill.go deleted file mode 100644 index 4cc3ee0fcb..0000000000 --- a/cli/command/container/kill.go +++ /dev/null @@ -1,56 +0,0 @@ -package container - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type killOptions struct { - signal string - - containers []string -} - -// NewKillCommand creates a new cobra.Command for `docker kill` -func NewKillCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts killOptions - - cmd := &cobra.Command{ - Use: "kill [OPTIONS] CONTAINER [CONTAINER...]", - Short: "Kill one or more running containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - return runKill(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.signal, "signal", "s", "KILL", "Signal to send to the container") - return cmd -} - -func runKill(dockerCli *command.DockerCli, opts *killOptions) error { - var errs []string - ctx := context.Background() - errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error { - return dockerCli.Client().ContainerKill(ctx, container, opts.signal) - }) - for _, name := range opts.containers { - if err := <-errChan; err != nil { - errs = append(errs, err.Error()) - } else { - fmt.Fprintln(dockerCli.Out(), name) - } - } - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/container/list.go b/cli/command/container/list.go deleted file mode 100644 index e0f4fdf21f..0000000000 --- a/cli/command/container/list.go +++ /dev/null @@ -1,140 +0,0 @@ -package container - -import ( - "io/ioutil" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/templates" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type psOptions struct { - quiet bool - size bool - all bool - noTrunc bool - nLatest bool - last int - format string - filter opts.FilterOpt -} - -// NewPsCommand creates a new cobra.Command for `docker ps` -func NewPsCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := psOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ps [OPTIONS]", - Short: "List containers", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runPs(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display numeric IDs") - flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes") - flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)") - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") - flags.BoolVarP(&opts.nLatest, "latest", "l", false, "Show the latest created container (includes all states)") - flags.IntVarP(&opts.last, "last", "n", -1, "Show n last created containers (includes all states)") - flags.StringVarP(&opts.format, "format", "", "", "Pretty-print containers using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - - return cmd -} - -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := *NewPsCommand(dockerCli) - cmd.Aliases = []string{"ps", "list"} - cmd.Use = "ls [OPTIONS]" - return &cmd -} - -// listOptionsProcessor is used to set any container list options which may only -// be embedded in the format template. -// This is passed directly into tmpl.Execute in order to allow the preprocessor -// to set any list options that were not provided by flags (e.g. `.Size`). -// It is using a `map[string]bool` so that unknown fields passed into the -// template format do not cause errors. These errors will get picked up when -// running through the actual template processor. -type listOptionsProcessor map[string]bool - -// Size sets the size of the map when called by a template execution. -func (o listOptionsProcessor) Size() bool { - o["size"] = true - return true -} - -// Label is needed here as it allows the correct pre-processing -// because Label() is a method with arguments -func (o listOptionsProcessor) Label(name string) string { - return "" -} - -func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, error) { - options := &types.ContainerListOptions{ - All: opts.all, - Limit: opts.last, - Size: opts.size, - Filters: opts.filter.Value(), - } - - if opts.nLatest && opts.last == -1 { - options.Limit = 1 - } - - tmpl, err := templates.Parse(opts.format) - - if err != nil { - return nil, err - } - - optionsProcessor := listOptionsProcessor{} - // This shouldn't error out but swallowing the error makes it harder - // to track down if preProcessor issues come up. Ref #24696 - if err := tmpl.Execute(ioutil.Discard, optionsProcessor); err != nil { - return nil, err - } - // At the moment all we need is to capture .Size for preprocessor - options.Size = opts.size || optionsProcessor["size"] - - return options, nil -} - -func runPs(dockerCli *command.DockerCli, opts *psOptions) error { - ctx := context.Background() - - listOptions, err := buildContainerListOptions(opts) - if err != nil { - return err - } - - containers, err := dockerCli.Client().ContainerList(ctx, *listOptions) - if err != nil { - return err - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().PsFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().PsFormat - } else { - format = formatter.TableFormatKey - } - } - - containerCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewContainerFormat(format, opts.quiet, listOptions.Size), - Trunc: !opts.noTrunc, - } - return formatter.ContainerWrite(containerCtx, containers) -} diff --git a/cli/command/container/logs.go b/cli/command/container/logs.go deleted file mode 100644 index d8cafaf744..0000000000 --- a/cli/command/container/logs.go +++ /dev/null @@ -1,76 +0,0 @@ -package container - -import ( - "io" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/stdcopy" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type logsOptions struct { - follow bool - since string - timestamps bool - details bool - tail string - - container string -} - -// NewLogsCommand creates a new cobra.Command for `docker logs` -func NewLogsCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts logsOptions - - cmd := &cobra.Command{ - Use: "logs [OPTIONS] CONTAINER", - Short: "Fetch the logs of a container", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.container = args[0] - return runLogs(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") - flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)") - flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") - flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs") - flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs") - return cmd -} - -func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { - ctx := context.Background() - - options := types.ContainerLogsOptions{ - ShowStdout: true, - ShowStderr: true, - Since: opts.since, - Timestamps: opts.timestamps, - Follow: opts.follow, - Tail: opts.tail, - Details: opts.details, - } - responseBody, err := dockerCli.Client().ContainerLogs(ctx, opts.container, options) - if err != nil { - return err - } - defer responseBody.Close() - - c, err := dockerCli.Client().ContainerInspect(ctx, opts.container) - if err != nil { - return err - } - - if c.Config.Tty { - _, err = io.Copy(dockerCli.Out(), responseBody) - } else { - _, err = stdcopy.StdCopy(dockerCli.Out(), dockerCli.Err(), responseBody) - } - return err -} diff --git a/cli/command/container/opts.go b/cli/command/container/opts.go deleted file mode 100644 index 2aaa3e750f..0000000000 --- a/cli/command/container/opts.go +++ /dev/null @@ -1,900 +0,0 @@ -package container - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "path" - "regexp" - "strconv" - "strings" - "time" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/types/container" - networktypes "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/strslice" - "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/signal" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/docker/go-connections/nat" - "github.com/pkg/errors" - "github.com/spf13/pflag" -) - -var ( - deviceCgroupRuleRegexp = regexp.MustCompile("^[acb] ([0-9]+|\\*):([0-9]+|\\*) [rwm]{1,3}$") -) - -// containerOptions is a data object with all the options for creating a container -type containerOptions struct { - attach opts.ListOpts - volumes opts.ListOpts - tmpfs opts.ListOpts - mounts opts.MountOpt - blkioWeightDevice opts.WeightdeviceOpt - deviceReadBps opts.ThrottledeviceOpt - deviceWriteBps opts.ThrottledeviceOpt - links opts.ListOpts - aliases opts.ListOpts - linkLocalIPs opts.ListOpts - deviceReadIOps opts.ThrottledeviceOpt - deviceWriteIOps opts.ThrottledeviceOpt - env opts.ListOpts - labels opts.ListOpts - deviceCgroupRules opts.ListOpts - devices opts.ListOpts - ulimits *opts.UlimitOpt - sysctls *opts.MapOpts - publish opts.ListOpts - expose opts.ListOpts - dns opts.ListOpts - dnsSearch opts.ListOpts - dnsOptions opts.ListOpts - extraHosts opts.ListOpts - volumesFrom opts.ListOpts - envFile opts.ListOpts - capAdd opts.ListOpts - capDrop opts.ListOpts - groupAdd opts.ListOpts - securityOpt opts.ListOpts - storageOpt opts.ListOpts - labelsFile opts.ListOpts - loggingOpts opts.ListOpts - privileged bool - pidMode string - utsMode string - usernsMode string - publishAll bool - stdin bool - tty bool - oomKillDisable bool - oomScoreAdj int - containerIDFile string - entrypoint string - hostname string - memory opts.MemBytes - memoryReservation opts.MemBytes - memorySwap opts.MemSwapBytes - kernelMemory opts.MemBytes - user string - workingDir string - cpuCount int64 - cpuShares int64 - cpuPercent int64 - cpuPeriod int64 - cpuRealtimePeriod int64 - cpuRealtimeRuntime int64 - cpuQuota int64 - cpus opts.NanoCPUs - cpusetCpus string - cpusetMems string - blkioWeight uint16 - ioMaxBandwidth opts.MemBytes - ioMaxIOps uint64 - swappiness int64 - netMode string - macAddress string - ipv4Address string - ipv6Address string - ipcMode string - pidsLimit int64 - restartPolicy string - readonlyRootfs bool - loggingDriver string - cgroupParent string - volumeDriver string - stopSignal string - stopTimeout int - isolation string - shmSize opts.MemBytes - noHealthcheck bool - healthCmd string - healthInterval time.Duration - healthTimeout time.Duration - healthStartPeriod time.Duration - healthRetries int - runtime string - autoRemove bool - init bool - - Image string - Args []string -} - -// addFlags adds all command line flags that will be used by parse to the FlagSet -func addFlags(flags *pflag.FlagSet) *containerOptions { - copts := &containerOptions{ - aliases: opts.NewListOpts(nil), - attach: opts.NewListOpts(validateAttach), - blkioWeightDevice: opts.NewWeightdeviceOpt(opts.ValidateWeightDevice), - capAdd: opts.NewListOpts(nil), - capDrop: opts.NewListOpts(nil), - dns: opts.NewListOpts(opts.ValidateIPAddress), - dnsOptions: opts.NewListOpts(nil), - dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), - deviceCgroupRules: opts.NewListOpts(validateDeviceCgroupRule), - deviceReadBps: opts.NewThrottledeviceOpt(opts.ValidateThrottleBpsDevice), - deviceReadIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice), - deviceWriteBps: opts.NewThrottledeviceOpt(opts.ValidateThrottleBpsDevice), - deviceWriteIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice), - devices: opts.NewListOpts(validateDevice), - env: opts.NewListOpts(opts.ValidateEnv), - envFile: opts.NewListOpts(nil), - expose: opts.NewListOpts(nil), - extraHosts: opts.NewListOpts(opts.ValidateExtraHost), - groupAdd: opts.NewListOpts(nil), - labels: opts.NewListOpts(opts.ValidateEnv), - labelsFile: opts.NewListOpts(nil), - linkLocalIPs: opts.NewListOpts(nil), - links: opts.NewListOpts(opts.ValidateLink), - loggingOpts: opts.NewListOpts(nil), - publish: opts.NewListOpts(nil), - securityOpt: opts.NewListOpts(nil), - storageOpt: opts.NewListOpts(nil), - sysctls: opts.NewMapOpts(nil, opts.ValidateSysctl), - tmpfs: opts.NewListOpts(nil), - ulimits: opts.NewUlimitOpt(nil), - volumes: opts.NewListOpts(nil), - volumesFrom: opts.NewListOpts(nil), - } - - // General purpose flags - flags.VarP(&copts.attach, "attach", "a", "Attach to STDIN, STDOUT or STDERR") - flags.Var(&copts.deviceCgroupRules, "device-cgroup-rule", "Add a rule to the cgroup allowed devices list") - flags.Var(&copts.devices, "device", "Add a host device to the container") - flags.VarP(&copts.env, "env", "e", "Set environment variables") - flags.Var(&copts.envFile, "env-file", "Read in a file of environment variables") - flags.StringVar(&copts.entrypoint, "entrypoint", "", "Overwrite the default ENTRYPOINT of the image") - flags.Var(&copts.groupAdd, "group-add", "Add additional groups to join") - flags.StringVarP(&copts.hostname, "hostname", "h", "", "Container host name") - flags.BoolVarP(&copts.stdin, "interactive", "i", false, "Keep STDIN open even if not attached") - flags.VarP(&copts.labels, "label", "l", "Set meta data on a container") - flags.Var(&copts.labelsFile, "label-file", "Read in a line delimited file of labels") - flags.BoolVar(&copts.readonlyRootfs, "read-only", false, "Mount the container's root filesystem as read only") - flags.StringVar(&copts.restartPolicy, "restart", "no", "Restart policy to apply when a container exits") - flags.StringVar(&copts.stopSignal, "stop-signal", signal.DefaultStopSignal, "Signal to stop a container") - flags.IntVar(&copts.stopTimeout, "stop-timeout", 0, "Timeout (in seconds) to stop a container") - flags.SetAnnotation("stop-timeout", "version", []string{"1.25"}) - flags.Var(copts.sysctls, "sysctl", "Sysctl options") - flags.BoolVarP(&copts.tty, "tty", "t", false, "Allocate a pseudo-TTY") - flags.Var(copts.ulimits, "ulimit", "Ulimit options") - flags.StringVarP(&copts.user, "user", "u", "", "Username or UID (format: [:])") - flags.StringVarP(&copts.workingDir, "workdir", "w", "", "Working directory inside the container") - flags.BoolVar(&copts.autoRemove, "rm", false, "Automatically remove the container when it exits") - - // Security - flags.Var(&copts.capAdd, "cap-add", "Add Linux capabilities") - flags.Var(&copts.capDrop, "cap-drop", "Drop Linux capabilities") - flags.BoolVar(&copts.privileged, "privileged", false, "Give extended privileges to this container") - flags.Var(&copts.securityOpt, "security-opt", "Security Options") - flags.StringVar(&copts.usernsMode, "userns", "", "User namespace to use") - - // Network and port publishing flag - flags.Var(&copts.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)") - flags.Var(&copts.dns, "dns", "Set custom DNS servers") - // We allow for both "--dns-opt" and "--dns-option", although the latter is the recommended way. - // This is to be consistent with service create/update - flags.Var(&copts.dnsOptions, "dns-opt", "Set DNS options") - flags.Var(&copts.dnsOptions, "dns-option", "Set DNS options") - flags.MarkHidden("dns-opt") - flags.Var(&copts.dnsSearch, "dns-search", "Set custom DNS search domains") - flags.Var(&copts.expose, "expose", "Expose a port or a range of ports") - flags.StringVar(&copts.ipv4Address, "ip", "", "IPv4 address (e.g., 172.30.100.104)") - flags.StringVar(&copts.ipv6Address, "ip6", "", "IPv6 address (e.g., 2001:db8::33)") - flags.Var(&copts.links, "link", "Add link to another container") - flags.Var(&copts.linkLocalIPs, "link-local-ip", "Container IPv4/IPv6 link-local addresses") - flags.StringVar(&copts.macAddress, "mac-address", "", "Container MAC address (e.g., 92:d0:c6:0a:29:33)") - flags.VarP(&copts.publish, "publish", "p", "Publish a container's port(s) to the host") - flags.BoolVarP(&copts.publishAll, "publish-all", "P", false, "Publish all exposed ports to random ports") - // We allow for both "--net" and "--network", although the latter is the recommended way. - flags.StringVar(&copts.netMode, "net", "default", "Connect a container to a network") - flags.StringVar(&copts.netMode, "network", "default", "Connect a container to a network") - flags.MarkHidden("net") - // We allow for both "--net-alias" and "--network-alias", although the latter is the recommended way. - flags.Var(&copts.aliases, "net-alias", "Add network-scoped alias for the container") - flags.Var(&copts.aliases, "network-alias", "Add network-scoped alias for the container") - flags.MarkHidden("net-alias") - - // Logging and storage - flags.StringVar(&copts.loggingDriver, "log-driver", "", "Logging driver for the container") - flags.StringVar(&copts.volumeDriver, "volume-driver", "", "Optional volume driver for the container") - flags.Var(&copts.loggingOpts, "log-opt", "Log driver options") - flags.Var(&copts.storageOpt, "storage-opt", "Storage driver options for the container") - flags.Var(&copts.tmpfs, "tmpfs", "Mount a tmpfs directory") - flags.Var(&copts.volumesFrom, "volumes-from", "Mount volumes from the specified container(s)") - flags.VarP(&copts.volumes, "volume", "v", "Bind mount a volume") - flags.Var(&copts.mounts, "mount", "Attach a filesystem mount to the container") - - // Health-checking - flags.StringVar(&copts.healthCmd, "health-cmd", "", "Command to run to check health") - flags.DurationVar(&copts.healthInterval, "health-interval", 0, "Time between running the check (ms|s|m|h) (default 0s)") - flags.IntVar(&copts.healthRetries, "health-retries", 0, "Consecutive failures needed to report unhealthy") - flags.DurationVar(&copts.healthTimeout, "health-timeout", 0, "Maximum time to allow one check to run (ms|s|m|h) (default 0s)") - flags.DurationVar(&copts.healthStartPeriod, "health-start-period", 0, "Start period for the container to initialize before starting health-retries countdown (ms|s|m|h) (default 0s)") - flags.SetAnnotation("health-start-period", "version", []string{"1.29"}) - flags.BoolVar(&copts.noHealthcheck, "no-healthcheck", false, "Disable any container-specified HEALTHCHECK") - - // Resource management - flags.Uint16Var(&copts.blkioWeight, "blkio-weight", 0, "Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)") - flags.Var(&copts.blkioWeightDevice, "blkio-weight-device", "Block IO weight (relative device weight)") - flags.StringVar(&copts.containerIDFile, "cidfile", "", "Write the container ID to the file") - flags.StringVar(&copts.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") - flags.StringVar(&copts.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") - flags.Int64Var(&copts.cpuCount, "cpu-count", 0, "CPU count (Windows only)") - flags.SetAnnotation("cpu-count", "ostype", []string{"windows"}) - flags.Int64Var(&copts.cpuPercent, "cpu-percent", 0, "CPU percent (Windows only)") - flags.SetAnnotation("cpu-percent", "ostype", []string{"windows"}) - flags.Int64Var(&copts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") - flags.Int64Var(&copts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota") - flags.Int64Var(&copts.cpuRealtimePeriod, "cpu-rt-period", 0, "Limit CPU real-time period in microseconds") - flags.SetAnnotation("cpu-rt-period", "version", []string{"1.25"}) - flags.Int64Var(&copts.cpuRealtimeRuntime, "cpu-rt-runtime", 0, "Limit CPU real-time runtime in microseconds") - flags.SetAnnotation("cpu-rt-runtime", "version", []string{"1.25"}) - flags.Int64VarP(&copts.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") - flags.Var(&copts.cpus, "cpus", "Number of CPUs") - flags.SetAnnotation("cpus", "version", []string{"1.25"}) - flags.Var(&copts.deviceReadBps, "device-read-bps", "Limit read rate (bytes per second) from a device") - flags.Var(&copts.deviceReadIOps, "device-read-iops", "Limit read rate (IO per second) from a device") - flags.Var(&copts.deviceWriteBps, "device-write-bps", "Limit write rate (bytes per second) to a device") - flags.Var(&copts.deviceWriteIOps, "device-write-iops", "Limit write rate (IO per second) to a device") - flags.Var(&copts.ioMaxBandwidth, "io-maxbandwidth", "Maximum IO bandwidth limit for the system drive (Windows only)") - flags.SetAnnotation("io-maxbandwidth", "ostype", []string{"windows"}) - flags.Uint64Var(&copts.ioMaxIOps, "io-maxiops", 0, "Maximum IOps limit for the system drive (Windows only)") - flags.SetAnnotation("io-maxiops", "ostype", []string{"windows"}) - flags.Var(&copts.kernelMemory, "kernel-memory", "Kernel memory limit") - flags.VarP(&copts.memory, "memory", "m", "Memory limit") - flags.Var(&copts.memoryReservation, "memory-reservation", "Memory soft limit") - flags.Var(&copts.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") - flags.Int64Var(&copts.swappiness, "memory-swappiness", -1, "Tune container memory swappiness (0 to 100)") - flags.BoolVar(&copts.oomKillDisable, "oom-kill-disable", false, "Disable OOM Killer") - flags.IntVar(&copts.oomScoreAdj, "oom-score-adj", 0, "Tune host's OOM preferences (-1000 to 1000)") - flags.Int64Var(&copts.pidsLimit, "pids-limit", 0, "Tune container pids limit (set -1 for unlimited)") - - // Low-level execution (cgroups, namespaces, ...) - flags.StringVar(&copts.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container") - flags.StringVar(&copts.ipcMode, "ipc", "", "IPC namespace to use") - flags.StringVar(&copts.isolation, "isolation", "", "Container isolation technology") - flags.StringVar(&copts.pidMode, "pid", "", "PID namespace to use") - flags.Var(&copts.shmSize, "shm-size", "Size of /dev/shm") - flags.StringVar(&copts.utsMode, "uts", "", "UTS namespace to use") - flags.StringVar(&copts.runtime, "runtime", "", "Runtime to use for this container") - - flags.BoolVar(&copts.init, "init", false, "Run an init inside the container that forwards signals and reaps processes") - flags.SetAnnotation("init", "version", []string{"1.25"}) - return copts -} - -type containerConfig struct { - Config *container.Config - HostConfig *container.HostConfig - NetworkingConfig *networktypes.NetworkingConfig -} - -// parse parses the args for the specified command and generates a Config, -// a HostConfig and returns them with the specified command. -// If the specified args are not valid, it will return an error. -func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, error) { - var ( - attachStdin = copts.attach.Get("stdin") - attachStdout = copts.attach.Get("stdout") - attachStderr = copts.attach.Get("stderr") - ) - - // Validate the input mac address - if copts.macAddress != "" { - if _, err := opts.ValidateMACAddress(copts.macAddress); err != nil { - return nil, errors.Errorf("%s is not a valid mac address", copts.macAddress) - } - } - if copts.stdin { - attachStdin = true - } - // If -a is not set, attach to stdout and stderr - if copts.attach.Len() == 0 { - attachStdout = true - attachStderr = true - } - - var err error - - swappiness := copts.swappiness - if swappiness != -1 && (swappiness < 0 || swappiness > 100) { - return nil, errors.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) - } - - mounts := copts.mounts.Value() - if len(mounts) > 0 && copts.volumeDriver != "" { - logrus.Warn("`--volume-driver` is ignored for volumes specified via `--mount`. Use `--mount type=volume,volume-driver=...` instead.") - } - var binds []string - volumes := copts.volumes.GetMap() - // add any bind targets to the list of container volumes - for bind := range copts.volumes.GetMap() { - if arr := volumeSplitN(bind, 2); len(arr) > 1 { - // after creating the bind mount we want to delete it from the copts.volumes values because - // we do not want bind mounts being committed to image configs - binds = append(binds, bind) - // We should delete from the map (`volumes`) here, as deleting from copts.volumes will not work if - // there are duplicates entries. - delete(volumes, bind) - } - } - - // Can't evaluate options passed into --tmpfs until we actually mount - tmpfs := make(map[string]string) - for _, t := range copts.tmpfs.GetAll() { - if arr := strings.SplitN(t, ":", 2); len(arr) > 1 { - tmpfs[arr[0]] = arr[1] - } else { - tmpfs[arr[0]] = "" - } - } - - var ( - runCmd strslice.StrSlice - entrypoint strslice.StrSlice - ) - - if len(copts.Args) > 0 { - runCmd = strslice.StrSlice(copts.Args) - } - - if copts.entrypoint != "" { - entrypoint = strslice.StrSlice{copts.entrypoint} - } else if flags.Changed("entrypoint") { - // if `--entrypoint=` is parsed then Entrypoint is reset - entrypoint = []string{""} - } - - ports, portBindings, err := nat.ParsePortSpecs(copts.publish.GetAll()) - if err != nil { - return nil, err - } - - // Merge in exposed ports to the map of published ports - for _, e := range copts.expose.GetAll() { - if strings.Contains(e, ":") { - return nil, errors.Errorf("invalid port format for --expose: %s", e) - } - //support two formats for expose, original format /[] or /[] - proto, port := nat.SplitProtoPort(e) - //parse the start and end port and create a sequence of ports to expose - //if expose a port, the start and end port are the same - start, end, err := nat.ParsePortRange(port) - if err != nil { - return nil, errors.Errorf("invalid range format for --expose: %s, error: %s", e, err) - } - for i := start; i <= end; i++ { - p, err := nat.NewPort(proto, strconv.FormatUint(i, 10)) - if err != nil { - return nil, err - } - if _, exists := ports[p]; !exists { - ports[p] = struct{}{} - } - } - } - - // parse device mappings - deviceMappings := []container.DeviceMapping{} - for _, device := range copts.devices.GetAll() { - deviceMapping, err := parseDevice(device) - if err != nil { - return nil, err - } - deviceMappings = append(deviceMappings, deviceMapping) - } - - // collect all the environment variables for the container - envVariables, err := runconfigopts.ReadKVStrings(copts.envFile.GetAll(), copts.env.GetAll()) - if err != nil { - return nil, err - } - - // collect all the labels for the container - labels, err := runconfigopts.ReadKVStrings(copts.labelsFile.GetAll(), copts.labels.GetAll()) - if err != nil { - return nil, err - } - - ipcMode := container.IpcMode(copts.ipcMode) - if !ipcMode.Valid() { - return nil, errors.Errorf("--ipc: invalid IPC mode") - } - - pidMode := container.PidMode(copts.pidMode) - if !pidMode.Valid() { - return nil, errors.Errorf("--pid: invalid PID mode") - } - - utsMode := container.UTSMode(copts.utsMode) - if !utsMode.Valid() { - return nil, errors.Errorf("--uts: invalid UTS mode") - } - - usernsMode := container.UsernsMode(copts.usernsMode) - if !usernsMode.Valid() { - return nil, errors.Errorf("--userns: invalid USER mode") - } - - restartPolicy, err := runconfigopts.ParseRestartPolicy(copts.restartPolicy) - if err != nil { - return nil, err - } - - loggingOpts, err := parseLoggingOpts(copts.loggingDriver, copts.loggingOpts.GetAll()) - if err != nil { - return nil, err - } - - securityOpts, err := parseSecurityOpts(copts.securityOpt.GetAll()) - if err != nil { - return nil, err - } - - storageOpts, err := parseStorageOpts(copts.storageOpt.GetAll()) - if err != nil { - return nil, err - } - - // Healthcheck - var healthConfig *container.HealthConfig - haveHealthSettings := copts.healthCmd != "" || - copts.healthInterval != 0 || - copts.healthTimeout != 0 || - copts.healthStartPeriod != 0 || - copts.healthRetries != 0 - if copts.noHealthcheck { - if haveHealthSettings { - return nil, errors.Errorf("--no-healthcheck conflicts with --health-* options") - } - test := strslice.StrSlice{"NONE"} - healthConfig = &container.HealthConfig{Test: test} - } else if haveHealthSettings { - var probe strslice.StrSlice - if copts.healthCmd != "" { - args := []string{"CMD-SHELL", copts.healthCmd} - probe = strslice.StrSlice(args) - } - if copts.healthInterval < 0 { - return nil, errors.Errorf("--health-interval cannot be negative") - } - if copts.healthTimeout < 0 { - return nil, errors.Errorf("--health-timeout cannot be negative") - } - if copts.healthRetries < 0 { - return nil, errors.Errorf("--health-retries cannot be negative") - } - if copts.healthStartPeriod < 0 { - return nil, fmt.Errorf("--health-start-period cannot be negative") - } - - healthConfig = &container.HealthConfig{ - Test: probe, - Interval: copts.healthInterval, - Timeout: copts.healthTimeout, - StartPeriod: copts.healthStartPeriod, - Retries: copts.healthRetries, - } - } - - resources := container.Resources{ - CgroupParent: copts.cgroupParent, - Memory: copts.memory.Value(), - MemoryReservation: copts.memoryReservation.Value(), - MemorySwap: copts.memorySwap.Value(), - MemorySwappiness: &copts.swappiness, - KernelMemory: copts.kernelMemory.Value(), - OomKillDisable: &copts.oomKillDisable, - NanoCPUs: copts.cpus.Value(), - CPUCount: copts.cpuCount, - CPUPercent: copts.cpuPercent, - CPUShares: copts.cpuShares, - CPUPeriod: copts.cpuPeriod, - CpusetCpus: copts.cpusetCpus, - CpusetMems: copts.cpusetMems, - CPUQuota: copts.cpuQuota, - CPURealtimePeriod: copts.cpuRealtimePeriod, - CPURealtimeRuntime: copts.cpuRealtimeRuntime, - PidsLimit: copts.pidsLimit, - BlkioWeight: copts.blkioWeight, - BlkioWeightDevice: copts.blkioWeightDevice.GetList(), - BlkioDeviceReadBps: copts.deviceReadBps.GetList(), - BlkioDeviceWriteBps: copts.deviceWriteBps.GetList(), - BlkioDeviceReadIOps: copts.deviceReadIOps.GetList(), - BlkioDeviceWriteIOps: copts.deviceWriteIOps.GetList(), - IOMaximumIOps: copts.ioMaxIOps, - IOMaximumBandwidth: uint64(copts.ioMaxBandwidth), - Ulimits: copts.ulimits.GetList(), - DeviceCgroupRules: copts.deviceCgroupRules.GetAll(), - Devices: deviceMappings, - } - - config := &container.Config{ - Hostname: copts.hostname, - ExposedPorts: ports, - User: copts.user, - Tty: copts.tty, - // TODO: deprecated, it comes from -n, --networking - // it's still needed internally to set the network to disabled - // if e.g. bridge is none in daemon opts, and in inspect - NetworkDisabled: false, - OpenStdin: copts.stdin, - AttachStdin: attachStdin, - AttachStdout: attachStdout, - AttachStderr: attachStderr, - Env: envVariables, - Cmd: runCmd, - Image: copts.Image, - Volumes: volumes, - MacAddress: copts.macAddress, - Entrypoint: entrypoint, - WorkingDir: copts.workingDir, - Labels: runconfigopts.ConvertKVStringsToMap(labels), - Healthcheck: healthConfig, - } - if flags.Changed("stop-signal") { - config.StopSignal = copts.stopSignal - } - if flags.Changed("stop-timeout") { - config.StopTimeout = &copts.stopTimeout - } - - hostConfig := &container.HostConfig{ - Binds: binds, - ContainerIDFile: copts.containerIDFile, - OomScoreAdj: copts.oomScoreAdj, - AutoRemove: copts.autoRemove, - Privileged: copts.privileged, - PortBindings: portBindings, - Links: copts.links.GetAll(), - PublishAllPorts: copts.publishAll, - // Make sure the dns fields are never nil. - // New containers don't ever have those fields nil, - // but pre created containers can still have those nil values. - // See https://github.com/docker/docker/pull/17779 - // for a more detailed explanation on why we don't want that. - DNS: copts.dns.GetAllOrEmpty(), - DNSSearch: copts.dnsSearch.GetAllOrEmpty(), - DNSOptions: copts.dnsOptions.GetAllOrEmpty(), - ExtraHosts: copts.extraHosts.GetAll(), - VolumesFrom: copts.volumesFrom.GetAll(), - NetworkMode: container.NetworkMode(copts.netMode), - IpcMode: ipcMode, - PidMode: pidMode, - UTSMode: utsMode, - UsernsMode: usernsMode, - CapAdd: strslice.StrSlice(copts.capAdd.GetAll()), - CapDrop: strslice.StrSlice(copts.capDrop.GetAll()), - GroupAdd: copts.groupAdd.GetAll(), - RestartPolicy: restartPolicy, - SecurityOpt: securityOpts, - StorageOpt: storageOpts, - ReadonlyRootfs: copts.readonlyRootfs, - LogConfig: container.LogConfig{Type: copts.loggingDriver, Config: loggingOpts}, - VolumeDriver: copts.volumeDriver, - Isolation: container.Isolation(copts.isolation), - ShmSize: copts.shmSize.Value(), - Resources: resources, - Tmpfs: tmpfs, - Sysctls: copts.sysctls.GetAll(), - Runtime: copts.runtime, - Mounts: mounts, - } - - if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() { - return nil, errors.Errorf("Conflicting options: --restart and --rm") - } - - // only set this value if the user provided the flag, else it should default to nil - if flags.Changed("init") { - hostConfig.Init = &copts.init - } - - // When allocating stdin in attached mode, close stdin at client disconnect - if config.OpenStdin && config.AttachStdin { - config.StdinOnce = true - } - - networkingConfig := &networktypes.NetworkingConfig{ - EndpointsConfig: make(map[string]*networktypes.EndpointSettings), - } - - if copts.ipv4Address != "" || copts.ipv6Address != "" || copts.linkLocalIPs.Len() > 0 { - epConfig := &networktypes.EndpointSettings{} - networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig - - epConfig.IPAMConfig = &networktypes.EndpointIPAMConfig{ - IPv4Address: copts.ipv4Address, - IPv6Address: copts.ipv6Address, - } - - if copts.linkLocalIPs.Len() > 0 { - epConfig.IPAMConfig.LinkLocalIPs = make([]string, copts.linkLocalIPs.Len()) - copy(epConfig.IPAMConfig.LinkLocalIPs, copts.linkLocalIPs.GetAll()) - } - } - - if hostConfig.NetworkMode.IsUserDefined() && len(hostConfig.Links) > 0 { - epConfig := networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] - if epConfig == nil { - epConfig = &networktypes.EndpointSettings{} - } - epConfig.Links = make([]string, len(hostConfig.Links)) - copy(epConfig.Links, hostConfig.Links) - networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig - } - - if copts.aliases.Len() > 0 { - epConfig := networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] - if epConfig == nil { - epConfig = &networktypes.EndpointSettings{} - } - epConfig.Aliases = make([]string, copts.aliases.Len()) - copy(epConfig.Aliases, copts.aliases.GetAll()) - networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig - } - - return &containerConfig{ - Config: config, - HostConfig: hostConfig, - NetworkingConfig: networkingConfig, - }, nil -} - -func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) { - loggingOptsMap := runconfigopts.ConvertKVStringsToMap(loggingOpts) - if loggingDriver == "none" && len(loggingOpts) > 0 { - return map[string]string{}, errors.Errorf("invalid logging opts for driver %s", loggingDriver) - } - return loggingOptsMap, nil -} - -// takes a local seccomp daemon, reads the file contents for sending to the daemon -func parseSecurityOpts(securityOpts []string) ([]string, error) { - for key, opt := range securityOpts { - con := strings.SplitN(opt, "=", 2) - if len(con) == 1 && con[0] != "no-new-privileges" { - if strings.Contains(opt, ":") { - con = strings.SplitN(opt, ":", 2) - } else { - return securityOpts, errors.Errorf("Invalid --security-opt: %q", opt) - } - } - if con[0] == "seccomp" && con[1] != "unconfined" { - f, err := ioutil.ReadFile(con[1]) - if err != nil { - return securityOpts, errors.Errorf("opening seccomp profile (%s) failed: %v", con[1], err) - } - b := bytes.NewBuffer(nil) - if err := json.Compact(b, f); err != nil { - return securityOpts, errors.Errorf("compacting json for seccomp profile (%s) failed: %v", con[1], err) - } - securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes()) - } - } - - return securityOpts, nil -} - -// parses storage options per container into a map -func parseStorageOpts(storageOpts []string) (map[string]string, error) { - m := make(map[string]string) - for _, option := range storageOpts { - if strings.Contains(option, "=") { - opt := strings.SplitN(option, "=", 2) - m[opt[0]] = opt[1] - } else { - return nil, errors.Errorf("invalid storage option") - } - } - return m, nil -} - -// parseDevice parses a device mapping string to a container.DeviceMapping struct -func parseDevice(device string) (container.DeviceMapping, error) { - src := "" - dst := "" - permissions := "rwm" - arr := strings.Split(device, ":") - switch len(arr) { - case 3: - permissions = arr[2] - fallthrough - case 2: - if validDeviceMode(arr[1]) { - permissions = arr[1] - } else { - dst = arr[1] - } - fallthrough - case 1: - src = arr[0] - default: - return container.DeviceMapping{}, errors.Errorf("invalid device specification: %s", device) - } - - if dst == "" { - dst = src - } - - deviceMapping := container.DeviceMapping{ - PathOnHost: src, - PathInContainer: dst, - CgroupPermissions: permissions, - } - return deviceMapping, nil -} - -// validateDeviceCgroupRule validates a device cgroup rule string format -// It will make sure 'val' is in the form: -// 'type major:minor mode' -func validateDeviceCgroupRule(val string) (string, error) { - if deviceCgroupRuleRegexp.MatchString(val) { - return val, nil - } - - return val, errors.Errorf("invalid device cgroup format '%s'", val) -} - -// validDeviceMode checks if the mode for device is valid or not. -// Valid mode is a composition of r (read), w (write), and m (mknod). -func validDeviceMode(mode string) bool { - var legalDeviceMode = map[rune]bool{ - 'r': true, - 'w': true, - 'm': true, - } - if mode == "" { - return false - } - for _, c := range mode { - if !legalDeviceMode[c] { - return false - } - legalDeviceMode[c] = false - } - return true -} - -// validateDevice validates a path for devices -// It will make sure 'val' is in the form: -// [host-dir:]container-path[:mode] -// It also validates the device mode. -func validateDevice(val string) (string, error) { - return validatePath(val, validDeviceMode) -} - -func validatePath(val string, validator func(string) bool) (string, error) { - var containerPath string - var mode string - - if strings.Count(val, ":") > 2 { - return val, errors.Errorf("bad format for path: %s", val) - } - - split := strings.SplitN(val, ":", 3) - if split[0] == "" { - return val, errors.Errorf("bad format for path: %s", val) - } - switch len(split) { - case 1: - containerPath = split[0] - val = path.Clean(containerPath) - case 2: - if isValid := validator(split[1]); isValid { - containerPath = split[0] - mode = split[1] - val = fmt.Sprintf("%s:%s", path.Clean(containerPath), mode) - } else { - containerPath = split[1] - val = fmt.Sprintf("%s:%s", split[0], path.Clean(containerPath)) - } - case 3: - containerPath = split[1] - mode = split[2] - if isValid := validator(split[2]); !isValid { - return val, errors.Errorf("bad mode specified: %s", mode) - } - val = fmt.Sprintf("%s:%s:%s", split[0], containerPath, mode) - } - - if !path.IsAbs(containerPath) { - return val, errors.Errorf("%s is not an absolute path", containerPath) - } - return val, nil -} - -// volumeSplitN splits raw into a maximum of n parts, separated by a separator colon. -// A separator colon is the last `:` character in the regex `[:\\]?[a-zA-Z]:` (note `\\` is `\` escaped). -// In Windows driver letter appears in two situations: -// a. `^[a-zA-Z]:` (A colon followed by `^[a-zA-Z]:` is OK as colon is the separator in volume option) -// b. A string in the format like `\\?\C:\Windows\...` (UNC). -// Therefore, a driver letter can only follow either a `:` or `\\` -// This allows to correctly split strings such as `C:\foo:D:\:rw` or `/tmp/q:/foo`. -func volumeSplitN(raw string, n int) []string { - var array []string - if len(raw) == 0 || raw[0] == ':' { - // invalid - return nil - } - // numberOfParts counts the number of parts separated by a separator colon - numberOfParts := 0 - // left represents the left-most cursor in raw, updated at every `:` character considered as a separator. - left := 0 - // right represents the right-most cursor in raw incremented with the loop. Note this - // starts at index 1 as index 0 is already handle above as a special case. - for right := 1; right < len(raw); right++ { - // stop parsing if reached maximum number of parts - if n >= 0 && numberOfParts >= n { - break - } - if raw[right] != ':' { - continue - } - potentialDriveLetter := raw[right-1] - if (potentialDriveLetter >= 'A' && potentialDriveLetter <= 'Z') || (potentialDriveLetter >= 'a' && potentialDriveLetter <= 'z') { - if right > 1 { - beforePotentialDriveLetter := raw[right-2] - // Only `:` or `\\` are checked (`/` could fall into the case of `/tmp/q:/foo`) - if beforePotentialDriveLetter != ':' && beforePotentialDriveLetter != '\\' { - // e.g. `C:` is not preceded by any delimiter, therefore it was not a drive letter but a path ending with `C:`. - array = append(array, raw[left:right]) - left = right + 1 - numberOfParts++ - } - // else, `C:` is considered as a drive letter and not as a delimiter, so we continue parsing. - } - // if right == 1, then `C:` is the beginning of the raw string, therefore `:` is again not considered a delimiter and we continue parsing. - } else { - // if `:` is not preceded by a potential drive letter, then consider it as a delimiter. - array = append(array, raw[left:right]) - left = right + 1 - numberOfParts++ - } - } - // need to take care of the last part - if left < len(raw) { - if n >= 0 && numberOfParts >= n { - // if the maximum number of parts is reached, just append the rest to the last part - // left-1 is at the last `:` that needs to be included since not considered a separator. - array[n-1] += raw[left-1:] - } else { - array = append(array, raw[left:]) - } - } - return array -} - -// validateAttach validates that the specified string is a valid attach option. -func validateAttach(val string) (string, error) { - s := strings.ToLower(val) - for _, str := range []string{"stdin", "stdout", "stderr"} { - if s == str { - return s, nil - } - } - return val, errors.Errorf("valid streams are STDIN, STDOUT and STDERR") -} diff --git a/cli/command/container/opts_test.go b/cli/command/container/opts_test.go deleted file mode 100644 index 2d0049768a..0000000000 --- a/cli/command/container/opts_test.go +++ /dev/null @@ -1,870 +0,0 @@ -package container - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "os" - "runtime" - "strings" - "testing" - "time" - - "github.com/docker/docker/api/types/container" - networktypes "github.com/docker/docker/api/types/network" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/runconfig" - "github.com/docker/go-connections/nat" - "github.com/pkg/errors" - "github.com/spf13/pflag" - "github.com/stretchr/testify/assert" -) - -func TestValidateAttach(t *testing.T) { - valid := []string{ - "stdin", - "stdout", - "stderr", - "STDIN", - "STDOUT", - "STDERR", - } - if _, err := validateAttach("invalid"); err == nil { - t.Fatal("Expected error with [valid streams are STDIN, STDOUT and STDERR], got nothing") - } - - for _, attach := range valid { - value, err := validateAttach(attach) - if err != nil { - t.Fatal(err) - } - if value != strings.ToLower(attach) { - t.Fatalf("Expected [%v], got [%v]", attach, value) - } - } -} - -func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) { - flags := pflag.NewFlagSet("run", pflag.ContinueOnError) - flags.SetOutput(ioutil.Discard) - flags.Usage = nil - copts := addFlags(flags) - if err := flags.Parse(args); err != nil { - return nil, nil, nil, err - } - // TODO: fix tests to accept ContainerConfig - containerConfig, err := parse(flags, copts) - if err != nil { - return nil, nil, nil, err - } - return containerConfig.Config, containerConfig.HostConfig, containerConfig.NetworkingConfig, err -} - -func parsetest(t *testing.T, args string) (*container.Config, *container.HostConfig, error) { - config, hostConfig, _, err := parseRun(strings.Split(args+" ubuntu bash", " ")) - return config, hostConfig, err -} - -func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig) { - config, hostConfig, err := parsetest(t, args) - if err != nil { - t.Fatal(err) - } - return config, hostConfig -} - -func TestParseRunLinks(t *testing.T) { - if _, hostConfig := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" { - t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links) - } - if _, hostConfig := mustParse(t, "--link a:b --link c:d"); len(hostConfig.Links) < 2 || hostConfig.Links[0] != "a:b" || hostConfig.Links[1] != "c:d" { - t.Fatalf("Error parsing links. Expected []string{\"a:b\", \"c:d\"}, received: %v", hostConfig.Links) - } - if _, hostConfig := mustParse(t, ""); len(hostConfig.Links) != 0 { - t.Fatalf("Error parsing links. No link expected, received: %v", hostConfig.Links) - } -} - -func TestParseRunAttach(t *testing.T) { - if config, _ := mustParse(t, "-a stdin"); !config.AttachStdin || config.AttachStdout || config.AttachStderr { - t.Fatalf("Error parsing attach flags. Expect only Stdin enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) - } - if config, _ := mustParse(t, "-a stdin -a stdout"); !config.AttachStdin || !config.AttachStdout || config.AttachStderr { - t.Fatalf("Error parsing attach flags. Expect only Stdin and Stdout enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) - } - if config, _ := mustParse(t, "-a stdin -a stdout -a stderr"); !config.AttachStdin || !config.AttachStdout || !config.AttachStderr { - t.Fatalf("Error parsing attach flags. Expect all attach enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) - } - if config, _ := mustParse(t, ""); config.AttachStdin || !config.AttachStdout || !config.AttachStderr { - t.Fatalf("Error parsing attach flags. Expect Stdin disabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) - } - if config, _ := mustParse(t, "-i"); !config.AttachStdin || !config.AttachStdout || !config.AttachStderr { - t.Fatalf("Error parsing attach flags. Expect Stdin enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) - } - - if _, _, err := parsetest(t, "-a"); err == nil { - t.Fatal("Error parsing attach flags, `-a` should be an error but is not") - } - if _, _, err := parsetest(t, "-a invalid"); err == nil { - t.Fatal("Error parsing attach flags, `-a invalid` should be an error but is not") - } - if _, _, err := parsetest(t, "-a invalid -a stdout"); err == nil { - t.Fatal("Error parsing attach flags, `-a stdout -a invalid` should be an error but is not") - } - if _, _, err := parsetest(t, "-a stdout -a stderr -d"); err == nil { - t.Fatal("Error parsing attach flags, `-a stdout -a stderr -d` should be an error but is not") - } - if _, _, err := parsetest(t, "-a stdin -d"); err == nil { - t.Fatal("Error parsing attach flags, `-a stdin -d` should be an error but is not") - } - if _, _, err := parsetest(t, "-a stdout -d"); err == nil { - t.Fatal("Error parsing attach flags, `-a stdout -d` should be an error but is not") - } - if _, _, err := parsetest(t, "-a stderr -d"); err == nil { - t.Fatal("Error parsing attach flags, `-a stderr -d` should be an error but is not") - } - if _, _, err := parsetest(t, "-d --rm"); err == nil { - t.Fatal("Error parsing attach flags, `-d --rm` should be an error but is not") - } -} - -func TestParseRunVolumes(t *testing.T) { - - // A single volume - arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`}) - if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil { - t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds) - } else if _, exists := config.Volumes[arr[0]]; !exists { - t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes) - } - - // Two volumes - arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`}) - if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil { - t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds) - } else if _, exists := config.Volumes[arr[0]]; !exists { - t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes) - } else if _, exists := config.Volumes[arr[1]]; !exists { - t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[1], config.Volumes) - } - - // A single bind-mount - arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`}) - if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] { - t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes) - } - - // Two bind-mounts. - arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`}) - if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { - t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) - } - - // Two bind-mounts, first read-only, second read-write. - // TODO Windows: The Windows version uses read-write as that's the only mode it supports. Can change this post TP4 - arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`}, []string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`}) - if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { - t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) - } - - // Similar to previous test but with alternate modes which are only supported by Linux - if runtime.GOOS != "windows" { - arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{}) - if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { - t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) - } - - arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{}) - if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { - t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) - } - } - - // One bind mount and one volume - arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`}) - if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] { - t.Fatalf("Error parsing volume flags, %s and %s should only one and only one bind mount %s. Received %s", arr[0], arr[1], arr[0], hostConfig.Binds) - } else if _, exists := config.Volumes[arr[1]]; !exists { - t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes) - } - - // Root to non-c: drive letter (Windows specific) - if runtime.GOOS == "windows" { - arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`}) - if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 { - t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0]) - } - } - -} - -// setupPlatformVolume takes two arrays of volume specs - a Unix style -// spec and a Windows style spec. Depending on the platform being unit tested, -// it returns one of them, along with a volume string that would be passed -// on the docker CLI (e.g. -v /bar -v /foo). -func setupPlatformVolume(u []string, w []string) ([]string, string) { - var a []string - if runtime.GOOS == "windows" { - a = w - } else { - a = u - } - s := "" - for _, v := range a { - s = s + "-v " + v + " " - } - return a, s -} - -// check if (a == c && b == d) || (a == d && b == c) -// because maps are randomized -func compareRandomizedStrings(a, b, c, d string) error { - if a == c && b == d { - return nil - } - if a == d && b == c { - return nil - } - return errors.Errorf("strings don't match") -} - -// Simple parse with MacAddress validation -func TestParseWithMacAddress(t *testing.T) { - invalidMacAddress := "--mac-address=invalidMacAddress" - validMacAddress := "--mac-address=92:d0:c6:0a:29:33" - if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" { - t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err) - } - if config, _ := mustParse(t, validMacAddress); config.MacAddress != "92:d0:c6:0a:29:33" { - t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as MacAddress, got '%v'", config.MacAddress) - } -} - -func TestParseWithMemory(t *testing.T) { - invalidMemory := "--memory=invalid" - _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}) - testutil.ErrorContains(t, err, invalidMemory) - - _, hostconfig := mustParse(t, "--memory=1G") - assert.Equal(t, int64(1073741824), hostconfig.Memory) -} - -func TestParseWithMemorySwap(t *testing.T) { - invalidMemory := "--memory-swap=invalid" - - _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}) - testutil.ErrorContains(t, err, invalidMemory) - - _, hostconfig := mustParse(t, "--memory-swap=1G") - assert.Equal(t, int64(1073741824), hostconfig.MemorySwap) - - _, hostconfig = mustParse(t, "--memory-swap=-1") - assert.Equal(t, int64(-1), hostconfig.MemorySwap) -} - -func TestParseHostname(t *testing.T) { - validHostnames := map[string]string{ - "hostname": "hostname", - "host-name": "host-name", - "hostname123": "hostname123", - "123hostname": "123hostname", - "hostname-of-63-bytes-long-should-be-valid-and-without-any-error": "hostname-of-63-bytes-long-should-be-valid-and-without-any-error", - } - hostnameWithDomain := "--hostname=hostname.domainname" - hostnameWithDomainTld := "--hostname=hostname.domainname.tld" - for hostname, expectedHostname := range validHostnames { - if config, _ := mustParse(t, fmt.Sprintf("--hostname=%s", hostname)); config.Hostname != expectedHostname { - t.Fatalf("Expected the config to have 'hostname' as hostname, got '%v'", config.Hostname) - } - } - if config, _ := mustParse(t, hostnameWithDomain); config.Hostname != "hostname.domainname" && config.Domainname != "" { - t.Fatalf("Expected the config to have 'hostname' as hostname.domainname, got '%v'", config.Hostname) - } - if config, _ := mustParse(t, hostnameWithDomainTld); config.Hostname != "hostname.domainname.tld" && config.Domainname != "" { - t.Fatalf("Expected the config to have 'hostname' as hostname.domainname.tld, got '%v'", config.Hostname) - } -} - -func TestParseWithExpose(t *testing.T) { - invalids := map[string]string{ - ":": "invalid port format for --expose: :", - "8080:9090": "invalid port format for --expose: 8080:9090", - "/tcp": "invalid range format for --expose: /tcp, error: Empty string specified for ports.", - "/udp": "invalid range format for --expose: /udp, error: Empty string specified for ports.", - "NaN/tcp": `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, - "NaN-NaN/tcp": `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, - "8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, - "1234567890-8080/tcp": `invalid range format for --expose: 1234567890-8080/tcp, error: strconv.ParseUint: parsing "1234567890": value out of range`, - } - valids := map[string][]nat.Port{ - "8080/tcp": {"8080/tcp"}, - "8080/udp": {"8080/udp"}, - "8080/ncp": {"8080/ncp"}, - "8080-8080/udp": {"8080/udp"}, - "8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"}, - } - for expose, expectedError := range invalids { - if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError { - t.Fatalf("Expected error '%v' with '--expose=%v', got '%v'", expectedError, expose, err) - } - } - for expose, exposedPorts := range valids { - config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if len(config.ExposedPorts) != len(exposedPorts) { - t.Fatalf("Expected %v exposed port, got %v", len(exposedPorts), len(config.ExposedPorts)) - } - for _, port := range exposedPorts { - if _, ok := config.ExposedPorts[port]; !ok { - t.Fatalf("Expected %v, got %v", exposedPorts, config.ExposedPorts) - } - } - } - // Merge with actual published port - config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if len(config.ExposedPorts) != 2 { - t.Fatalf("Expected 2 exposed ports, got %v", config.ExposedPorts) - } - ports := []nat.Port{"80/tcp", "81/tcp"} - for _, port := range ports { - if _, ok := config.ExposedPorts[port]; !ok { - t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts) - } - } -} - -func TestParseDevice(t *testing.T) { - valids := map[string]container.DeviceMapping{ - "/dev/snd": { - PathOnHost: "/dev/snd", - PathInContainer: "/dev/snd", - CgroupPermissions: "rwm", - }, - "/dev/snd:rw": { - PathOnHost: "/dev/snd", - PathInContainer: "/dev/snd", - CgroupPermissions: "rw", - }, - "/dev/snd:/something": { - PathOnHost: "/dev/snd", - PathInContainer: "/something", - CgroupPermissions: "rwm", - }, - "/dev/snd:/something:rw": { - PathOnHost: "/dev/snd", - PathInContainer: "/something", - CgroupPermissions: "rw", - }, - } - for device, deviceMapping := range valids { - _, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--device=%v", device), "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if len(hostconfig.Devices) != 1 { - t.Fatalf("Expected 1 devices, got %v", hostconfig.Devices) - } - if hostconfig.Devices[0] != deviceMapping { - t.Fatalf("Expected %v, got %v", deviceMapping, hostconfig.Devices) - } - } - -} - -func TestParseModes(t *testing.T) { - // ipc ko - if _, _, _, err := parseRun([]string{"--ipc=container:", "img", "cmd"}); err == nil || err.Error() != "--ipc: invalid IPC mode" { - t.Fatalf("Expected an error with message '--ipc: invalid IPC mode', got %v", err) - } - // ipc ok - _, hostconfig, _, err := parseRun([]string{"--ipc=host", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if !hostconfig.IpcMode.Valid() { - t.Fatalf("Expected a valid IpcMode, got %v", hostconfig.IpcMode) - } - // pid ko - if _, _, _, err := parseRun([]string{"--pid=container:", "img", "cmd"}); err == nil || err.Error() != "--pid: invalid PID mode" { - t.Fatalf("Expected an error with message '--pid: invalid PID mode', got %v", err) - } - // pid ok - _, hostconfig, _, err = parseRun([]string{"--pid=host", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if !hostconfig.PidMode.Valid() { - t.Fatalf("Expected a valid PidMode, got %v", hostconfig.PidMode) - } - // uts ko - if _, _, _, err := parseRun([]string{"--uts=container:", "img", "cmd"}); err == nil || err.Error() != "--uts: invalid UTS mode" { - t.Fatalf("Expected an error with message '--uts: invalid UTS mode', got %v", err) - } - // uts ok - _, hostconfig, _, err = parseRun([]string{"--uts=host", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if !hostconfig.UTSMode.Valid() { - t.Fatalf("Expected a valid UTSMode, got %v", hostconfig.UTSMode) - } - // shm-size ko - expectedErr := `invalid argument "a128m" for --shm-size=a128m: invalid size: 'a128m'` - if _, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"}); err == nil || err.Error() != expectedErr { - t.Fatalf("Expected an error with message '%v', got %v", expectedErr, err) - } - // shm-size ok - _, hostconfig, _, err = parseRun([]string{"--shm-size=128m", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if hostconfig.ShmSize != 134217728 { - t.Fatalf("Expected a valid ShmSize, got %d", hostconfig.ShmSize) - } -} - -func TestParseRestartPolicy(t *testing.T) { - invalids := map[string]string{ - "always:2:3": "invalid restart policy format", - "on-failure:invalid": "maximum retry count must be an integer", - } - valids := map[string]container.RestartPolicy{ - "": {}, - "always": { - Name: "always", - MaximumRetryCount: 0, - }, - "on-failure:1": { - Name: "on-failure", - MaximumRetryCount: 1, - }, - } - for restart, expectedError := range invalids { - if _, _, _, err := parseRun([]string{fmt.Sprintf("--restart=%s", restart), "img", "cmd"}); err == nil || err.Error() != expectedError { - t.Fatalf("Expected an error with message '%v' for %v, got %v", expectedError, restart, err) - } - } - for restart, expected := range valids { - _, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--restart=%v", restart), "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if hostconfig.RestartPolicy != expected { - t.Fatalf("Expected %v, got %v", expected, hostconfig.RestartPolicy) - } - } -} - -func TestParseRestartPolicyAutoRemove(t *testing.T) { - expected := "Conflicting options: --restart and --rm" - _, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) - if err == nil || err.Error() != expected { - t.Fatalf("Expected error %v, but got none", expected) - } -} - -func TestParseHealth(t *testing.T) { - checkOk := func(args ...string) *container.HealthConfig { - config, _, _, err := parseRun(args) - if err != nil { - t.Fatalf("%#v: %v", args, err) - } - return config.Healthcheck - } - checkError := func(expected string, args ...string) { - config, _, _, err := parseRun(args) - if err == nil { - t.Fatalf("Expected error, but got %#v", config) - } - if err.Error() != expected { - t.Fatalf("Expected %#v, got %#v", expected, err) - } - } - health := checkOk("--no-healthcheck", "img", "cmd") - if health == nil || len(health.Test) != 1 || health.Test[0] != "NONE" { - t.Fatalf("--no-healthcheck failed: %#v", health) - } - - health = checkOk("--health-cmd=/check.sh -q", "img", "cmd") - if len(health.Test) != 2 || health.Test[0] != "CMD-SHELL" || health.Test[1] != "/check.sh -q" { - t.Fatalf("--health-cmd: got %#v", health.Test) - } - if health.Timeout != 0 { - t.Fatalf("--health-cmd: timeout = %s", health.Timeout) - } - - checkError("--no-healthcheck conflicts with --health-* options", - "--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd") - - health = checkOk("--health-timeout=2s", "--health-retries=3", "--health-interval=4.5s", "--health-start-period=5s", "img", "cmd") - if health.Timeout != 2*time.Second || health.Retries != 3 || health.Interval != 4500*time.Millisecond || health.StartPeriod != 5*time.Second { - t.Fatalf("--health-*: got %#v", health) - } -} - -func TestParseLoggingOpts(t *testing.T) { - // logging opts ko - if _, _, _, err := parseRun([]string{"--log-driver=none", "--log-opt=anything", "img", "cmd"}); err == nil || err.Error() != "invalid logging opts for driver none" { - t.Fatalf("Expected an error with message 'invalid logging opts for driver none', got %v", err) - } - // logging opts ok - _, hostconfig, _, err := parseRun([]string{"--log-driver=syslog", "--log-opt=something", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if hostconfig.LogConfig.Type != "syslog" || len(hostconfig.LogConfig.Config) != 1 { - t.Fatalf("Expected a 'syslog' LogConfig with one config, got %v", hostconfig.RestartPolicy) - } -} - -func TestParseEnvfileVariables(t *testing.T) { - e := "open nonexistent: no such file or directory" - if runtime.GOOS == "windows" { - e = "open nonexistent: The system cannot find the file specified." - } - // env ko - if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { - t.Fatalf("Expected an error with message '%s', got %v", e, err) - } - // env ok - config, _, _, err := parseRun([]string{"--env-file=testdata/valid.env", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if len(config.Env) != 1 || config.Env[0] != "ENV1=value1" { - t.Fatalf("Expected a config with [ENV1=value1], got %v", config.Env) - } - config, _, _, err = parseRun([]string{"--env-file=testdata/valid.env", "--env=ENV2=value2", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if len(config.Env) != 2 || config.Env[0] != "ENV1=value1" || config.Env[1] != "ENV2=value2" { - t.Fatalf("Expected a config with [ENV1=value1 ENV2=value2], got %v", config.Env) - } -} - -func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) { - // UTF8 with BOM - config, _, _, err := parseRun([]string{"--env-file=testdata/utf8.env", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - env := []string{"FOO=BAR", "HELLO=" + string([]byte{0xe6, 0x82, 0xa8, 0xe5, 0xa5, 0xbd}), "BAR=FOO"} - if len(config.Env) != len(env) { - t.Fatalf("Expected a config with %d env variables, got %v: %v", len(env), len(config.Env), config.Env) - } - for i, v := range env { - if config.Env[i] != v { - t.Fatalf("Expected a config with [%s], got %v", v, []byte(config.Env[i])) - } - } - - // UTF16 with BOM - e := "contains invalid utf8 bytes at line" - if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) { - t.Fatalf("Expected an error with message '%s', got %v", e, err) - } - // UTF16BE with BOM - if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16be.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) { - t.Fatalf("Expected an error with message '%s', got %v", e, err) - } -} - -func TestParseLabelfileVariables(t *testing.T) { - e := "open nonexistent: no such file or directory" - if runtime.GOOS == "windows" { - e = "open nonexistent: The system cannot find the file specified." - } - // label ko - if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { - t.Fatalf("Expected an error with message '%s', got %v", e, err) - } - // label ok - config, _, _, err := parseRun([]string{"--label-file=testdata/valid.label", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if len(config.Labels) != 1 || config.Labels["LABEL1"] != "value1" { - t.Fatalf("Expected a config with [LABEL1:value1], got %v", config.Labels) - } - config, _, _, err = parseRun([]string{"--label-file=testdata/valid.label", "--label=LABEL2=value2", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if len(config.Labels) != 2 || config.Labels["LABEL1"] != "value1" || config.Labels["LABEL2"] != "value2" { - t.Fatalf("Expected a config with [LABEL1:value1 LABEL2:value2], got %v", config.Labels) - } -} - -func TestParseEntryPoint(t *testing.T) { - config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"}) - if err != nil { - t.Fatal(err) - } - if len(config.Entrypoint) != 1 && config.Entrypoint[0] != "anything" { - t.Fatalf("Expected entrypoint 'anything', got %v", config.Entrypoint) - } -} - -// This tests the cases for binds which are generated through -// DecodeContainerConfig rather than Parse() -func TestDecodeContainerConfigVolumes(t *testing.T) { - - // Root to root - bindsOrVols, _ := setupPlatformVolume([]string{`/:/`}, []string{os.Getenv("SystemDrive") + `\:c:\`}) - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // No destination path - bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:`}, []string{os.Getenv("TEMP") + `\:`}) - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // // No destination path or mode - bindsOrVols, _ = setupPlatformVolume([]string{`/tmp::`}, []string{os.Getenv("TEMP") + `\::`}) - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // A whole lot of nothing - bindsOrVols = []string{`:`} - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // A whole lot of nothing with no mode - bindsOrVols = []string{`::`} - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // Too much including an invalid mode - wTmp := os.Getenv("TEMP") - bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:/tmp:/tmp:/tmp`}, []string{wTmp + ":" + wTmp + ":" + wTmp + ":" + wTmp}) - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // Windows specific error tests - if runtime.GOOS == "windows" { - // Volume which does not include a drive letter - bindsOrVols = []string{`\tmp`} - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // Root to C-Drive - bindsOrVols = []string{os.Getenv("SystemDrive") + `\:c:`} - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // Container path that does not include a drive letter - bindsOrVols = []string{`c:\windows:\somewhere`} - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - } - - // Linux-specific error tests - if runtime.GOOS != "windows" { - // Just root - bindsOrVols = []string{`/`} - if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { - t.Fatalf("binds %v should have failed", bindsOrVols) - } - if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { - t.Fatalf("volume %v should have failed", bindsOrVols) - } - - // A single volume that looks like a bind mount passed in Volumes. - // This should be handled as a bind mount, not a volume. - vols := []string{`/foo:/bar`} - if config, hostConfig, err := callDecodeContainerConfig(vols, nil); err != nil { - t.Fatal("Volume /foo:/bar should have succeeded as a volume name") - } else if hostConfig.Binds != nil { - t.Fatalf("Error parsing volume flags, /foo:/bar should not mount-bind anything. Received %v", hostConfig.Binds) - } else if _, exists := config.Volumes[vols[0]]; !exists { - t.Fatalf("Error parsing volume flags, /foo:/bar is missing from volumes. Received %v", config.Volumes) - } - - } -} - -// callDecodeContainerConfig is a utility function used by TestDecodeContainerConfigVolumes -// to call DecodeContainerConfig. It effectively does what a client would -// do when calling the daemon by constructing a JSON stream of a -// ContainerConfigWrapper which is populated by the set of volume specs -// passed into it. It returns a config and a hostconfig which can be -// validated to ensure DecodeContainerConfig has manipulated the structures -// correctly. -func callDecodeContainerConfig(volumes []string, binds []string) (*container.Config, *container.HostConfig, error) { - var ( - b []byte - err error - c *container.Config - h *container.HostConfig - ) - w := runconfig.ContainerConfigWrapper{ - Config: &container.Config{ - Volumes: map[string]struct{}{}, - }, - HostConfig: &container.HostConfig{ - NetworkMode: "none", - Binds: binds, - }, - } - for _, v := range volumes { - w.Config.Volumes[v] = struct{}{} - } - if b, err = json.Marshal(w); err != nil { - return nil, nil, errors.Errorf("Error on marshal %s", err.Error()) - } - c, h, _, err = runconfig.DecodeContainerConfig(bytes.NewReader(b)) - if err != nil { - return nil, nil, errors.Errorf("Error parsing %s: %v", string(b), err) - } - if c == nil || h == nil { - return nil, nil, errors.Errorf("Empty config or hostconfig") - } - - return c, h, err -} - -func TestVolumeSplitN(t *testing.T) { - for _, x := range []struct { - input string - n int - expected []string - }{ - {`C:\foo:d:`, -1, []string{`C:\foo`, `d:`}}, - {`:C:\foo:d:`, -1, nil}, - {`/foo:/bar:ro`, 3, []string{`/foo`, `/bar`, `ro`}}, - {`/foo:/bar:ro`, 2, []string{`/foo`, `/bar:ro`}}, - {`C:\foo\:/foo`, -1, []string{`C:\foo\`, `/foo`}}, - - {`d:\`, -1, []string{`d:\`}}, - {`d:`, -1, []string{`d:`}}, - {`d:\path`, -1, []string{`d:\path`}}, - {`d:\path with space`, -1, []string{`d:\path with space`}}, - {`d:\pathandmode:rw`, -1, []string{`d:\pathandmode`, `rw`}}, - {`c:\:d:\`, -1, []string{`c:\`, `d:\`}}, - {`c:\windows\:d:`, -1, []string{`c:\windows\`, `d:`}}, - {`c:\windows:d:\s p a c e`, -1, []string{`c:\windows`, `d:\s p a c e`}}, - {`c:\windows:d:\s p a c e:RW`, -1, []string{`c:\windows`, `d:\s p a c e`, `RW`}}, - {`c:\program files:d:\s p a c e i n h o s t d i r`, -1, []string{`c:\program files`, `d:\s p a c e i n h o s t d i r`}}, - {`0123456789name:d:`, -1, []string{`0123456789name`, `d:`}}, - {`MiXeDcAsEnAmE:d:`, -1, []string{`MiXeDcAsEnAmE`, `d:`}}, - {`name:D:`, -1, []string{`name`, `D:`}}, - {`name:D::rW`, -1, []string{`name`, `D:`, `rW`}}, - {`name:D::RW`, -1, []string{`name`, `D:`, `RW`}}, - {`c:/:d:/forward/slashes/are/good/too`, -1, []string{`c:/`, `d:/forward/slashes/are/good/too`}}, - {`c:\Windows`, -1, []string{`c:\Windows`}}, - {`c:\Program Files (x86)`, -1, []string{`c:\Program Files (x86)`}}, - - {``, -1, nil}, - {`.`, -1, []string{`.`}}, - {`..\`, -1, []string{`..\`}}, - {`c:\:..\`, -1, []string{`c:\`, `..\`}}, - {`c:\:d:\:xyzzy`, -1, []string{`c:\`, `d:\`, `xyzzy`}}, - - // Cover directories with one-character name - {`/tmp/x/y:/foo/x/y`, -1, []string{`/tmp/x/y`, `/foo/x/y`}}, - } { - res := volumeSplitN(x.input, x.n) - if len(res) < len(x.expected) { - t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res) - } - for i, e := range res { - if e != x.expected[i] { - t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res) - } - } - } -} - -func TestValidateDevice(t *testing.T) { - valid := []string{ - "/home", - "/home:/home", - "/home:/something/else", - "/with space", - "/home:/with space", - "relative:/absolute-path", - "hostPath:/containerPath:r", - "/hostPath:/containerPath:rw", - "/hostPath:/containerPath:mrw", - } - invalid := map[string]string{ - "": "bad format for path: ", - "./": "./ is not an absolute path", - "../": "../ is not an absolute path", - "/:../": "../ is not an absolute path", - "/:path": "path is not an absolute path", - ":": "bad format for path: :", - "/tmp:": " is not an absolute path", - ":test": "bad format for path: :test", - ":/test": "bad format for path: :/test", - "tmp:": " is not an absolute path", - ":test:": "bad format for path: :test:", - "::": "bad format for path: ::", - ":::": "bad format for path: :::", - "/tmp:::": "bad format for path: /tmp:::", - ":/tmp::": "bad format for path: :/tmp::", - "path:ro": "ro is not an absolute path", - "path:rr": "rr is not an absolute path", - "a:/b:ro": "bad mode specified: ro", - "a:/b:rr": "bad mode specified: rr", - } - - for _, path := range valid { - if _, err := validateDevice(path); err != nil { - t.Fatalf("ValidateDevice(`%q`) should succeed: error %q", path, err) - } - } - - for path, expectedError := range invalid { - if _, err := validateDevice(path); err == nil { - t.Fatalf("ValidateDevice(`%q`) should have failed validation", path) - } else { - if err.Error() != expectedError { - t.Fatalf("ValidateDevice(`%q`) error should contain %q, got %q", path, expectedError, err.Error()) - } - } - } -} diff --git a/cli/command/container/pause.go b/cli/command/container/pause.go deleted file mode 100644 index 095a0db2c2..0000000000 --- a/cli/command/container/pause.go +++ /dev/null @@ -1,49 +0,0 @@ -package container - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type pauseOptions struct { - containers []string -} - -// NewPauseCommand creates a new cobra.Command for `docker pause` -func NewPauseCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts pauseOptions - - return &cobra.Command{ - Use: "pause CONTAINER [CONTAINER...]", - Short: "Pause all processes within one or more containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - return runPause(dockerCli, &opts) - }, - } -} - -func runPause(dockerCli *command.DockerCli, opts *pauseOptions) error { - ctx := context.Background() - - var errs []string - errChan := parallelOperation(ctx, opts.containers, dockerCli.Client().ContainerPause) - for _, container := range opts.containers { - if err := <-errChan; err != nil { - errs = append(errs, err.Error()) - continue - } - fmt.Fprintln(dockerCli.Out(), container) - } - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/container/port.go b/cli/command/container/port.go deleted file mode 100644 index 2793f6bc6b..0000000000 --- a/cli/command/container/port.go +++ /dev/null @@ -1,78 +0,0 @@ -package container - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/go-connections/nat" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type portOptions struct { - container string - - port string -} - -// NewPortCommand creates a new cobra.Command for `docker port` -func NewPortCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts portOptions - - cmd := &cobra.Command{ - Use: "port CONTAINER [PRIVATE_PORT[/PROTO]]", - Short: "List port mappings or a specific mapping for the container", - Args: cli.RequiresRangeArgs(1, 2), - RunE: func(cmd *cobra.Command, args []string) error { - opts.container = args[0] - if len(args) > 1 { - opts.port = args[1] - } - return runPort(dockerCli, &opts) - }, - } - return cmd -} - -func runPort(dockerCli *command.DockerCli, opts *portOptions) error { - ctx := context.Background() - - c, err := dockerCli.Client().ContainerInspect(ctx, opts.container) - if err != nil { - return err - } - - if opts.port != "" { - port := opts.port - proto := "tcp" - parts := strings.SplitN(port, "/", 2) - - if len(parts) == 2 && len(parts[1]) != 0 { - port = parts[0] - proto = parts[1] - } - natPort := port + "/" + proto - newP, err := nat.NewPort(proto, port) - if err != nil { - return err - } - if frontends, exists := c.NetworkSettings.Ports[newP]; exists && frontends != nil { - for _, frontend := range frontends { - fmt.Fprintf(dockerCli.Out(), "%s:%s\n", frontend.HostIP, frontend.HostPort) - } - return nil - } - return errors.Errorf("Error: No public port '%s' published for %s", natPort, opts.container) - } - - for from, frontends := range c.NetworkSettings.Ports { - for _, frontend := range frontends { - fmt.Fprintf(dockerCli.Out(), "%s -> %s:%s\n", from, frontend.HostIP, frontend.HostPort) - } - } - - return nil -} diff --git a/cli/command/container/prune.go b/cli/command/container/prune.go deleted file mode 100644 index cf12dc71fe..0000000000 --- a/cli/command/container/prune.go +++ /dev/null @@ -1,78 +0,0 @@ -package container - -import ( - "fmt" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - units "github.com/docker/go-units" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type pruneOptions struct { - force bool - filter opts.FilterOpt -} - -// NewPruneCommand returns a new cobra prune command for containers -func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := pruneOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "prune [OPTIONS]", - Short: "Remove all stopped containers", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - spaceReclaimed, output, err := runPrune(dockerCli, opts) - if err != nil { - return err - } - if output != "" { - fmt.Fprintln(dockerCli.Out(), output) - } - fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) - return nil - }, - Tags: map[string]string{"version": "1.25"}, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") - flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=')") - - return cmd -} - -const warning = `WARNING! This will remove all stopped containers. -Are you sure you want to continue?` - -func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { - pruneFilters := command.PruneFilters(dockerCli, opts.filter.Value()) - - if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { - return - } - - report, err := dockerCli.Client().ContainersPrune(context.Background(), pruneFilters) - if err != nil { - return - } - - if len(report.ContainersDeleted) > 0 { - output = "Deleted Containers:\n" - for _, id := range report.ContainersDeleted { - output += id + "\n" - } - spaceReclaimed = report.SpaceReclaimed - } - - return -} - -// RunPrune calls the Container Prune API -// This returns the amount of space reclaimed and a detailed output string -func RunPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { - return runPrune(dockerCli, pruneOptions{force: true, filter: filter}) -} diff --git a/cli/command/container/ps_test.go b/cli/command/container/ps_test.go deleted file mode 100644 index 47665b0e2c..0000000000 --- a/cli/command/container/ps_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package container - -import ( - "testing" - - "github.com/docker/docker/opts" - "github.com/stretchr/testify/assert" -) - -func TestBuildContainerListOptions(t *testing.T) { - filters := opts.NewFilterOpt() - assert.NoError(t, filters.Set("foo=bar")) - assert.NoError(t, filters.Set("baz=foo")) - - contexts := []struct { - psOpts *psOptions - expectedAll bool - expectedSize bool - expectedLimit int - expectedFilters map[string]string - }{ - { - psOpts: &psOptions{ - all: true, - size: true, - last: 5, - filter: filters, - }, - expectedAll: true, - expectedSize: true, - expectedLimit: 5, - expectedFilters: map[string]string{ - "foo": "bar", - "baz": "foo", - }, - }, - { - psOpts: &psOptions{ - all: true, - size: true, - last: -1, - nLatest: true, - }, - expectedAll: true, - expectedSize: true, - expectedLimit: 1, - expectedFilters: make(map[string]string), - }, - { - psOpts: &psOptions{ - all: true, - size: false, - last: 5, - filter: filters, - // With .Size, size should be true - format: "{{.Size}}", - }, - expectedAll: true, - expectedSize: true, - expectedLimit: 5, - expectedFilters: map[string]string{ - "foo": "bar", - "baz": "foo", - }, - }, - { - psOpts: &psOptions{ - all: true, - size: false, - last: 5, - filter: filters, - // With .Size, size should be true - format: "{{.Size}} {{.CreatedAt}} {{.Networks}}", - }, - expectedAll: true, - expectedSize: true, - expectedLimit: 5, - expectedFilters: map[string]string{ - "foo": "bar", - "baz": "foo", - }, - }, - { - psOpts: &psOptions{ - all: true, - size: false, - last: 5, - filter: filters, - // Without .Size, size should be false - format: "{{.CreatedAt}} {{.Networks}}", - }, - expectedAll: true, - expectedSize: false, - expectedLimit: 5, - expectedFilters: map[string]string{ - "foo": "bar", - "baz": "foo", - }, - }, - } - - for _, c := range contexts { - options, err := buildContainerListOptions(c.psOpts) - assert.NoError(t, err) - - assert.Equal(t, c.expectedAll, options.All) - assert.Equal(t, c.expectedSize, options.Size) - assert.Equal(t, c.expectedLimit, options.Limit) - assert.Equal(t, len(c.expectedFilters), options.Filters.Len()) - - for k, v := range c.expectedFilters { - f := options.Filters - if !f.ExactMatch(k, v) { - t.Fatalf("Expected filter with key %s to be %s but got %s", k, v, f.Get(k)) - } - } - } -} diff --git a/cli/command/container/rename.go b/cli/command/container/rename.go deleted file mode 100644 index 07b4852f47..0000000000 --- a/cli/command/container/rename.go +++ /dev/null @@ -1,51 +0,0 @@ -package container - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type renameOptions struct { - oldName string - newName string -} - -// NewRenameCommand creates a new cobra.Command for `docker rename` -func NewRenameCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts renameOptions - - cmd := &cobra.Command{ - Use: "rename CONTAINER NEW_NAME", - Short: "Rename a container", - Args: cli.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - opts.oldName = args[0] - opts.newName = args[1] - return runRename(dockerCli, &opts) - }, - } - return cmd -} - -func runRename(dockerCli *command.DockerCli, opts *renameOptions) error { - ctx := context.Background() - - oldName := strings.TrimSpace(opts.oldName) - newName := strings.TrimSpace(opts.newName) - - if oldName == "" || newName == "" { - return errors.New("Error: Neither old nor new names may be empty") - } - - if err := dockerCli.Client().ContainerRename(ctx, oldName, newName); err != nil { - fmt.Fprintln(dockerCli.Err(), err) - return errors.Errorf("Error: failed to rename container named %s", oldName) - } - return nil -} diff --git a/cli/command/container/restart.go b/cli/command/container/restart.go deleted file mode 100644 index 73cd2507ee..0000000000 --- a/cli/command/container/restart.go +++ /dev/null @@ -1,62 +0,0 @@ -package container - -import ( - "fmt" - "strings" - "time" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type restartOptions struct { - nSeconds int - nSecondsChanged bool - - containers []string -} - -// NewRestartCommand creates a new cobra.Command for `docker restart` -func NewRestartCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts restartOptions - - cmd := &cobra.Command{ - Use: "restart [OPTIONS] CONTAINER [CONTAINER...]", - Short: "Restart one or more containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - opts.nSecondsChanged = cmd.Flags().Changed("time") - return runRestart(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.IntVarP(&opts.nSeconds, "time", "t", 10, "Seconds to wait for stop before killing the container") - return cmd -} - -func runRestart(dockerCli *command.DockerCli, opts *restartOptions) error { - ctx := context.Background() - var errs []string - var timeout *time.Duration - if opts.nSecondsChanged { - timeoutValue := time.Duration(opts.nSeconds) * time.Second - timeout = &timeoutValue - } - - for _, name := range opts.containers { - if err := dockerCli.Client().ContainerRestart(ctx, name, timeout); err != nil { - errs = append(errs, err.Error()) - continue - } - fmt.Fprintln(dockerCli.Out(), name) - } - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/container/rm.go b/cli/command/container/rm.go deleted file mode 100644 index 887b5c5d34..0000000000 --- a/cli/command/container/rm.go +++ /dev/null @@ -1,73 +0,0 @@ -package container - -import ( - "fmt" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type rmOptions struct { - rmVolumes bool - rmLink bool - force bool - - containers []string -} - -// NewRmCommand creates a new cobra.Command for `docker rm` -func NewRmCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts rmOptions - - cmd := &cobra.Command{ - Use: "rm [OPTIONS] CONTAINER [CONTAINER...]", - Short: "Remove one or more containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - return runRm(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.rmVolumes, "volumes", "v", false, "Remove the volumes associated with the container") - flags.BoolVarP(&opts.rmLink, "link", "l", false, "Remove the specified link") - flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of a running container (uses SIGKILL)") - return cmd -} - -func runRm(dockerCli *command.DockerCli, opts *rmOptions) error { - ctx := context.Background() - - var errs []string - options := types.ContainerRemoveOptions{ - RemoveVolumes: opts.rmVolumes, - RemoveLinks: opts.rmLink, - Force: opts.force, - } - - errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error { - container = strings.Trim(container, "/") - if container == "" { - return errors.New("Container name cannot be empty") - } - return dockerCli.Client().ContainerRemove(ctx, container, options) - }) - - for _, name := range opts.containers { - if err := <-errChan; err != nil { - errs = append(errs, err.Error()) - continue - } - fmt.Fprintln(dockerCli.Out(), name) - } - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/container/run.go b/cli/command/container/run.go deleted file mode 100644 index bab6a9cf13..0000000000 --- a/cli/command/container/run.go +++ /dev/null @@ -1,296 +0,0 @@ -package container - -import ( - "fmt" - "io" - "net/http/httputil" - "os" - "runtime" - "strings" - "syscall" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/promise" - "github.com/docker/docker/pkg/signal" - "github.com/docker/libnetwork/resolvconf/dns" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "golang.org/x/net/context" -) - -type runOptions struct { - detach bool - sigProxy bool - name string - detachKeys string -} - -// NewRunCommand create a new `docker run` command -func NewRunCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts runOptions - var copts *containerOptions - - cmd := &cobra.Command{ - Use: "run [OPTIONS] IMAGE [COMMAND] [ARG...]", - Short: "Run a command in a new container", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - copts.Image = args[0] - if len(args) > 1 { - copts.Args = args[1:] - } - return runRun(dockerCli, cmd.Flags(), &opts, copts) - }, - } - - flags := cmd.Flags() - flags.SetInterspersed(false) - - // These are flags not stored in Config/HostConfig - flags.BoolVarP(&opts.detach, "detach", "d", false, "Run container in background and print container ID") - flags.BoolVar(&opts.sigProxy, "sig-proxy", true, "Proxy received signals to the process") - flags.StringVar(&opts.name, "name", "", "Assign a name to the container") - flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container") - - // Add an explicit help that doesn't have a `-h` to prevent the conflict - // with hostname - flags.Bool("help", false, "Print usage") - - command.AddTrustVerificationFlags(flags) - copts = addFlags(flags) - return cmd -} - -func warnOnOomKillDisable(hostConfig container.HostConfig, stderr io.Writer) { - if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 { - fmt.Fprintln(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.") - } -} - -// check the DNS settings passed via --dns against localhost regexp to warn if -// they are trying to set a DNS to a localhost address -func warnOnLocalhostDNS(hostConfig container.HostConfig, stderr io.Writer) { - for _, dnsIP := range hostConfig.DNS { - if dns.IsLocalhost(dnsIP) { - fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP) - return - } - } -} - -func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions, copts *containerOptions) error { - containerConfig, err := parse(flags, copts) - // just in case the parse does not exit - if err != nil { - reportError(dockerCli.Err(), "run", err.Error(), true) - return cli.StatusError{StatusCode: 125} - } - return runContainer(dockerCli, opts, copts, containerConfig) -} - -func runContainer(dockerCli *command.DockerCli, opts *runOptions, copts *containerOptions, containerConfig *containerConfig) error { - config := containerConfig.Config - hostConfig := containerConfig.HostConfig - stdout, stderr := dockerCli.Out(), dockerCli.Err() - client := dockerCli.Client() - - // TODO: pass this as an argument - cmdPath := "run" - - warnOnOomKillDisable(*hostConfig, stderr) - warnOnLocalhostDNS(*hostConfig, stderr) - - config.ArgsEscaped = false - - if !opts.detach { - if err := dockerCli.In().CheckTty(config.AttachStdin, config.Tty); err != nil { - return err - } - } else { - if copts.attach.Len() != 0 { - return errors.New("Conflicting options: -a and -d") - } - - config.AttachStdin = false - config.AttachStdout = false - config.AttachStderr = false - config.StdinOnce = false - } - - // Disable sigProxy when in TTY mode - if config.Tty { - opts.sigProxy = false - } - - // Telling the Windows daemon the initial size of the tty during start makes - // a far better user experience rather than relying on subsequent resizes - // to cause things to catch up. - if runtime.GOOS == "windows" { - hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize() - } - - ctx, cancelFun := context.WithCancel(context.Background()) - - createResponse, err := createContainer(ctx, dockerCli, containerConfig, opts.name) - if err != nil { - reportError(stderr, cmdPath, err.Error(), true) - return runStartContainerErr(err) - } - if opts.sigProxy { - sigc := ForwardAllSignals(ctx, dockerCli, createResponse.ID) - defer signal.StopCatch(sigc) - } - var ( - waitDisplayID chan struct{} - errCh chan error - ) - if !config.AttachStdout && !config.AttachStderr { - // Make this asynchronous to allow the client to write to stdin before having to read the ID - waitDisplayID = make(chan struct{}) - go func() { - defer close(waitDisplayID) - fmt.Fprintln(stdout, createResponse.ID) - }() - } - attach := config.AttachStdin || config.AttachStdout || config.AttachStderr - if attach { - if opts.detachKeys != "" { - dockerCli.ConfigFile().DetachKeys = opts.detachKeys - } - - close, err := attachContainer(ctx, dockerCli, &errCh, config, createResponse.ID) - defer close() - if err != nil { - return err - } - } - - statusChan := waitExitOrRemoved(ctx, dockerCli, createResponse.ID, copts.autoRemove) - - //start the container - if err := client.ContainerStart(ctx, createResponse.ID, types.ContainerStartOptions{}); err != nil { - // If we have holdHijackedConnection, we should notify - // holdHijackedConnection we are going to exit and wait - // to avoid the terminal are not restored. - if attach { - cancelFun() - <-errCh - } - - reportError(stderr, cmdPath, err.Error(), false) - if copts.autoRemove { - // wait container to be removed - <-statusChan - } - return runStartContainerErr(err) - } - - if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && dockerCli.Out().IsTerminal() { - if err := MonitorTtySize(ctx, dockerCli, createResponse.ID, false); err != nil { - fmt.Fprintln(stderr, "Error monitoring TTY size:", err) - } - } - - if errCh != nil { - if err := <-errCh; err != nil { - logrus.Debugf("Error hijack: %s", err) - return err - } - } - - // Detached mode: wait for the id to be displayed and return. - if !config.AttachStdout && !config.AttachStderr { - // Detached mode - <-waitDisplayID - return nil - } - - status := <-statusChan - if status != 0 { - return cli.StatusError{StatusCode: status} - } - return nil -} - -func attachContainer( - ctx context.Context, - dockerCli *command.DockerCli, - errCh *chan error, - config *container.Config, - containerID string, -) (func(), error) { - stdout, stderr := dockerCli.Out(), dockerCli.Err() - var ( - out, cerr io.Writer - in io.ReadCloser - ) - if config.AttachStdin { - in = dockerCli.In() - } - if config.AttachStdout { - out = stdout - } - if config.AttachStderr { - if config.Tty { - cerr = stdout - } else { - cerr = stderr - } - } - - options := types.ContainerAttachOptions{ - Stream: true, - Stdin: config.AttachStdin, - Stdout: config.AttachStdout, - Stderr: config.AttachStderr, - DetachKeys: dockerCli.ConfigFile().DetachKeys, - } - - resp, errAttach := dockerCli.Client().ContainerAttach(ctx, containerID, options) - if errAttach != nil && errAttach != httputil.ErrPersistEOF { - // ContainerAttach returns an ErrPersistEOF (connection closed) - // means server met an error and put it in Hijacked connection - // keep the error and read detailed error message from hijacked connection later - return nil, errAttach - } - - *errCh = promise.Go(func() error { - if errHijack := holdHijackedConnection(ctx, dockerCli, config.Tty, in, out, cerr, resp); errHijack != nil { - return errHijack - } - return errAttach - }) - return resp.Close, nil -} - -// reportError is a utility method that prints a user-friendly message -// containing the error that occurred during parsing and a suggestion to get help -func reportError(stderr io.Writer, name string, str string, withHelp bool) { - str = strings.TrimSuffix(str, ".") + "." - if withHelp { - str += "\nSee '" + os.Args[0] + " " + name + " --help'." - } - fmt.Fprintf(stderr, "%s: %s\n", os.Args[0], str) -} - -// if container start fails with 'not found'/'no such' error, return 127 -// if container start fails with 'permission denied' error, return 126 -// return 125 for generic docker daemon failures -func runStartContainerErr(err error) error { - trimmedErr := strings.TrimPrefix(err.Error(), "Error response from daemon: ") - statusError := cli.StatusError{StatusCode: 125} - if strings.Contains(trimmedErr, "executable file not found") || - strings.Contains(trimmedErr, "no such file or directory") || - strings.Contains(trimmedErr, "system cannot find the file specified") { - statusError = cli.StatusError{StatusCode: 127} - } else if strings.Contains(trimmedErr, syscall.EACCES.Error()) { - statusError = cli.StatusError{StatusCode: 126} - } - - return statusError -} diff --git a/cli/command/container/start.go b/cli/command/container/start.go deleted file mode 100644 index 7702cd4a75..0000000000 --- a/cli/command/container/start.go +++ /dev/null @@ -1,179 +0,0 @@ -package container - -import ( - "fmt" - "io" - "net/http/httputil" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/promise" - "github.com/docker/docker/pkg/signal" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type startOptions struct { - attach bool - openStdin bool - detachKeys string - checkpoint string - checkpointDir string - - containers []string -} - -// NewStartCommand creates a new cobra.Command for `docker start` -func NewStartCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts startOptions - - cmd := &cobra.Command{ - Use: "start [OPTIONS] CONTAINER [CONTAINER...]", - Short: "Start one or more stopped containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - return runStart(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.attach, "attach", "a", false, "Attach STDOUT/STDERR and forward signals") - flags.BoolVarP(&opts.openStdin, "interactive", "i", false, "Attach container's STDIN") - flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container") - - flags.StringVar(&opts.checkpoint, "checkpoint", "", "Restore from this checkpoint") - flags.SetAnnotation("checkpoint", "experimental", nil) - flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory") - flags.SetAnnotation("checkpoint-dir", "experimental", nil) - return cmd -} - -func runStart(dockerCli *command.DockerCli, opts *startOptions) error { - ctx, cancelFun := context.WithCancel(context.Background()) - - if opts.attach || opts.openStdin { - // We're going to attach to a container. - // 1. Ensure we only have one container. - if len(opts.containers) > 1 { - return errors.New("You cannot start and attach multiple containers at once.") - } - - // 2. Attach to the container. - container := opts.containers[0] - c, err := dockerCli.Client().ContainerInspect(ctx, container) - if err != nil { - return err - } - - // We always use c.ID instead of container to maintain consistency during `docker start` - if !c.Config.Tty { - sigc := ForwardAllSignals(ctx, dockerCli, c.ID) - defer signal.StopCatch(sigc) - } - - if opts.detachKeys != "" { - dockerCli.ConfigFile().DetachKeys = opts.detachKeys - } - - options := types.ContainerAttachOptions{ - Stream: true, - Stdin: opts.openStdin && c.Config.OpenStdin, - Stdout: true, - Stderr: true, - DetachKeys: dockerCli.ConfigFile().DetachKeys, - } - - var in io.ReadCloser - - if options.Stdin { - in = dockerCli.In() - } - - resp, errAttach := dockerCli.Client().ContainerAttach(ctx, c.ID, options) - if errAttach != nil && errAttach != httputil.ErrPersistEOF { - // ContainerAttach return an ErrPersistEOF (connection closed) - // means server met an error and already put it in Hijacked connection, - // we would keep the error and read the detailed error message from hijacked connection - return errAttach - } - defer resp.Close() - cErr := promise.Go(func() error { - errHijack := holdHijackedConnection(ctx, dockerCli, c.Config.Tty, in, dockerCli.Out(), dockerCli.Err(), resp) - if errHijack == nil { - return errAttach - } - return errHijack - }) - - // 3. We should open a channel for receiving status code of the container - // no matter it's detached, removed on daemon side(--rm) or exit normally. - statusChan := waitExitOrRemoved(ctx, dockerCli, c.ID, c.HostConfig.AutoRemove) - startOptions := types.ContainerStartOptions{ - CheckpointID: opts.checkpoint, - CheckpointDir: opts.checkpointDir, - } - - // 4. Start the container. - if err := dockerCli.Client().ContainerStart(ctx, c.ID, startOptions); err != nil { - cancelFun() - <-cErr - if c.HostConfig.AutoRemove { - // wait container to be removed - <-statusChan - } - return err - } - - // 5. Wait for attachment to break. - if c.Config.Tty && dockerCli.Out().IsTerminal() { - if err := MonitorTtySize(ctx, dockerCli, c.ID, false); err != nil { - fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err) - } - } - if attchErr := <-cErr; attchErr != nil { - return attchErr - } - - if status := <-statusChan; status != 0 { - return cli.StatusError{StatusCode: status} - } - } else if opts.checkpoint != "" { - if len(opts.containers) > 1 { - return errors.New("You cannot restore multiple containers at once.") - } - container := opts.containers[0] - startOptions := types.ContainerStartOptions{ - CheckpointID: opts.checkpoint, - CheckpointDir: opts.checkpointDir, - } - return dockerCli.Client().ContainerStart(ctx, container, startOptions) - - } else { - // We're not going to attach to anything. - // Start as many containers as we want. - return startContainersWithoutAttachments(ctx, dockerCli, opts.containers) - } - - return nil -} - -func startContainersWithoutAttachments(ctx context.Context, dockerCli *command.DockerCli, containers []string) error { - var failedContainers []string - for _, container := range containers { - if err := dockerCli.Client().ContainerStart(ctx, container, types.ContainerStartOptions{}); err != nil { - fmt.Fprintln(dockerCli.Err(), err) - failedContainers = append(failedContainers, container) - continue - } - fmt.Fprintln(dockerCli.Out(), container) - } - - if len(failedContainers) > 0 { - return errors.Errorf("Error: failed to start containers: %s", strings.Join(failedContainers, ", ")) - } - return nil -} diff --git a/cli/command/container/stats.go b/cli/command/container/stats.go deleted file mode 100644 index c420e8151e..0000000000 --- a/cli/command/container/stats.go +++ /dev/null @@ -1,242 +0,0 @@ -package container - -import ( - "fmt" - "io" - "strings" - "sync" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/events" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type statsOptions struct { - all bool - noStream bool - format string - containers []string -} - -// NewStatsCommand creates a new cobra.Command for `docker stats` -func NewStatsCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts statsOptions - - cmd := &cobra.Command{ - Use: "stats [OPTIONS] [CONTAINER...]", - Short: "Display a live stream of container(s) resource usage statistics", - Args: cli.RequiresMinArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - return runStats(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)") - flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result") - flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template") - return cmd -} - -// runStats displays a live stream of resource usage statistics for one or more containers. -// This shows real-time information on CPU usage, memory usage, and network I/O. -func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { - showAll := len(opts.containers) == 0 - closeChan := make(chan error) - - ctx := context.Background() - - // monitorContainerEvents watches for container creation and removal (only - // used when calling `docker stats` without arguments). - monitorContainerEvents := func(started chan<- struct{}, c chan events.Message) { - f := filters.NewArgs() - f.Add("type", "container") - options := types.EventsOptions{ - Filters: f, - } - - eventq, errq := dockerCli.Client().Events(ctx, options) - - // Whether we successfully subscribed to eventq or not, we can now - // unblock the main goroutine. - close(started) - - for { - select { - case event := <-eventq: - c <- event - case err := <-errq: - closeChan <- err - return - } - } - } - - // Get the daemonOSType if not set already - if daemonOSType == "" { - svctx := context.Background() - sv, err := dockerCli.Client().ServerVersion(svctx) - if err != nil { - return err - } - daemonOSType = sv.Os - } - - // waitFirst is a WaitGroup to wait first stat data's reach for each container - waitFirst := &sync.WaitGroup{} - - cStats := stats{} - // getContainerList simulates creation event for all previously existing - // containers (only used when calling `docker stats` without arguments). - getContainerList := func() { - options := types.ContainerListOptions{ - All: opts.all, - } - cs, err := dockerCli.Client().ContainerList(ctx, options) - if err != nil { - closeChan <- err - } - for _, container := range cs { - s := formatter.NewContainerStats(container.ID[:12], daemonOSType) - if cStats.add(s) { - waitFirst.Add(1) - go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) - } - } - } - - if showAll { - // If no names were specified, start a long running goroutine which - // monitors container events. We make sure we're subscribed before - // retrieving the list of running containers to avoid a race where we - // would "miss" a creation. - started := make(chan struct{}) - eh := command.InitEventHandler() - eh.Handle("create", func(e events.Message) { - if opts.all { - s := formatter.NewContainerStats(e.ID[:12], daemonOSType) - if cStats.add(s) { - waitFirst.Add(1) - go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) - } - } - }) - - eh.Handle("start", func(e events.Message) { - s := formatter.NewContainerStats(e.ID[:12], daemonOSType) - if cStats.add(s) { - waitFirst.Add(1) - go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) - } - }) - - eh.Handle("die", func(e events.Message) { - if !opts.all { - cStats.remove(e.ID[:12]) - } - }) - - eventChan := make(chan events.Message) - go eh.Watch(eventChan) - go monitorContainerEvents(started, eventChan) - defer close(eventChan) - <-started - - // Start a short-lived goroutine to retrieve the initial list of - // containers. - getContainerList() - } else { - // Artificially send creation events for the containers we were asked to - // monitor (same code path than we use when monitoring all containers). - for _, name := range opts.containers { - s := formatter.NewContainerStats(name, daemonOSType) - if cStats.add(s) { - waitFirst.Add(1) - go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) - } - } - - // We don't expect any asynchronous errors: closeChan can be closed. - close(closeChan) - - // Do a quick pause to detect any error with the provided list of - // container names. - time.Sleep(1500 * time.Millisecond) - var errs []string - cStats.mu.Lock() - for _, c := range cStats.cs { - if err := c.GetError(); err != nil { - errs = append(errs, err.Error()) - } - } - cStats.mu.Unlock() - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - } - - // before print to screen, make sure each container get at least one valid stat data - waitFirst.Wait() - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().StatsFormat) > 0 { - format = dockerCli.ConfigFile().StatsFormat - } else { - format = formatter.TableFormatKey - } - } - statsCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewStatsFormat(format, daemonOSType), - } - cleanScreen := func() { - if !opts.noStream { - fmt.Fprint(dockerCli.Out(), "\033[2J") - fmt.Fprint(dockerCli.Out(), "\033[H") - } - } - - var err error - for range time.Tick(500 * time.Millisecond) { - cleanScreen() - ccstats := []formatter.StatsEntry{} - cStats.mu.Lock() - for _, c := range cStats.cs { - ccstats = append(ccstats, c.GetStatistics()) - } - cStats.mu.Unlock() - if err = formatter.ContainerStatsWrite(statsCtx, ccstats, daemonOSType); err != nil { - break - } - if len(cStats.cs) == 0 && !showAll { - break - } - if opts.noStream { - break - } - select { - case err, ok := <-closeChan: - if ok { - if err != nil { - // this is suppressing "unexpected EOF" in the cli when the - // daemon restarts so it shutdowns cleanly - if err == io.ErrUnexpectedEOF { - return nil - } - return err - } - } - default: - // just skip - } - } - return err -} diff --git a/cli/command/container/stats_helpers.go b/cli/command/container/stats_helpers.go deleted file mode 100644 index 5cbcf03e40..0000000000 --- a/cli/command/container/stats_helpers.go +++ /dev/null @@ -1,229 +0,0 @@ -package container - -import ( - "encoding/json" - "io" - "strings" - "sync" - "time" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/client" - "github.com/pkg/errors" - "golang.org/x/net/context" -) - -type stats struct { - ostype string - mu sync.Mutex - cs []*formatter.ContainerStats -} - -// daemonOSType is set once we have at least one stat for a container -// from the daemon. It is used to ensure we print the right header based -// on the daemon platform. -var daemonOSType string - -func (s *stats) add(cs *formatter.ContainerStats) bool { - s.mu.Lock() - defer s.mu.Unlock() - if _, exists := s.isKnownContainer(cs.Container); !exists { - s.cs = append(s.cs, cs) - return true - } - return false -} - -func (s *stats) remove(id string) { - s.mu.Lock() - if i, exists := s.isKnownContainer(id); exists { - s.cs = append(s.cs[:i], s.cs[i+1:]...) - } - s.mu.Unlock() -} - -func (s *stats) isKnownContainer(cid string) (int, bool) { - for i, c := range s.cs { - if c.Container == cid { - return i, true - } - } - return -1, false -} - -func collect(ctx context.Context, s *formatter.ContainerStats, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) { - logrus.Debugf("collecting stats for %s", s.Container) - var ( - getFirst bool - previousCPU uint64 - previousSystem uint64 - u = make(chan error, 1) - ) - - defer func() { - // if error happens and we get nothing of stats, release wait group whatever - if !getFirst { - getFirst = true - waitFirst.Done() - } - }() - - response, err := cli.ContainerStats(ctx, s.Container, streamStats) - if err != nil { - s.SetError(err) - return - } - defer response.Body.Close() - - dec := json.NewDecoder(response.Body) - go func() { - for { - var ( - v *types.StatsJSON - memPercent, cpuPercent float64 - blkRead, blkWrite uint64 // Only used on Linux - mem, memLimit, memPerc float64 - pidsStatsCurrent uint64 - ) - - if err := dec.Decode(&v); err != nil { - dec = json.NewDecoder(io.MultiReader(dec.Buffered(), response.Body)) - u <- err - if err == io.EOF { - break - } - time.Sleep(100 * time.Millisecond) - continue - } - - daemonOSType = response.OSType - - if daemonOSType != "windows" { - // MemoryStats.Limit will never be 0 unless the container is not running and we haven't - // got any data from cgroup - if v.MemoryStats.Limit != 0 { - memPercent = float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0 - } - previousCPU = v.PreCPUStats.CPUUsage.TotalUsage - previousSystem = v.PreCPUStats.SystemUsage - cpuPercent = calculateCPUPercentUnix(previousCPU, previousSystem, v) - blkRead, blkWrite = calculateBlockIO(v.BlkioStats) - mem = float64(v.MemoryStats.Usage) - memLimit = float64(v.MemoryStats.Limit) - memPerc = memPercent - pidsStatsCurrent = v.PidsStats.Current - } else { - cpuPercent = calculateCPUPercentWindows(v) - blkRead = v.StorageStats.ReadSizeBytes - blkWrite = v.StorageStats.WriteSizeBytes - mem = float64(v.MemoryStats.PrivateWorkingSet) - } - netRx, netTx := calculateNetwork(v.Networks) - s.SetStatistics(formatter.StatsEntry{ - Name: v.Name, - ID: v.ID, - CPUPercentage: cpuPercent, - Memory: mem, - MemoryPercentage: memPerc, - MemoryLimit: memLimit, - NetworkRx: netRx, - NetworkTx: netTx, - BlockRead: float64(blkRead), - BlockWrite: float64(blkWrite), - PidsCurrent: pidsStatsCurrent, - }) - u <- nil - if !streamStats { - return - } - } - }() - for { - select { - case <-time.After(2 * time.Second): - // zero out the values if we have not received an update within - // the specified duration. - s.SetErrorAndReset(errors.New("timeout waiting for stats")) - // if this is the first stat you get, release WaitGroup - if !getFirst { - getFirst = true - waitFirst.Done() - } - case err := <-u: - s.SetError(err) - if err == io.EOF { - break - } - if err != nil { - continue - } - // if this is the first stat you get, release WaitGroup - if !getFirst { - getFirst = true - waitFirst.Done() - } - } - if !streamStats { - return - } - } -} - -func calculateCPUPercentUnix(previousCPU, previousSystem uint64, v *types.StatsJSON) float64 { - var ( - cpuPercent = 0.0 - // calculate the change for the cpu usage of the container in between readings - cpuDelta = float64(v.CPUStats.CPUUsage.TotalUsage) - float64(previousCPU) - // calculate the change for the entire system between readings - systemDelta = float64(v.CPUStats.SystemUsage) - float64(previousSystem) - onlineCPUs = float64(v.CPUStats.OnlineCPUs) - ) - - if onlineCPUs == 0.0 { - onlineCPUs = float64(len(v.CPUStats.CPUUsage.PercpuUsage)) - } - if systemDelta > 0.0 && cpuDelta > 0.0 { - cpuPercent = (cpuDelta / systemDelta) * onlineCPUs * 100.0 - } - return cpuPercent -} - -func calculateCPUPercentWindows(v *types.StatsJSON) float64 { - // Max number of 100ns intervals between the previous time read and now - possIntervals := uint64(v.Read.Sub(v.PreRead).Nanoseconds()) // Start with number of ns intervals - possIntervals /= 100 // Convert to number of 100ns intervals - possIntervals *= uint64(v.NumProcs) // Multiple by the number of processors - - // Intervals used - intervalsUsed := v.CPUStats.CPUUsage.TotalUsage - v.PreCPUStats.CPUUsage.TotalUsage - - // Percentage avoiding divide-by-zero - if possIntervals > 0 { - return float64(intervalsUsed) / float64(possIntervals) * 100.0 - } - return 0.00 -} - -func calculateBlockIO(blkio types.BlkioStats) (blkRead uint64, blkWrite uint64) { - for _, bioEntry := range blkio.IoServiceBytesRecursive { - switch strings.ToLower(bioEntry.Op) { - case "read": - blkRead = blkRead + bioEntry.Value - case "write": - blkWrite = blkWrite + bioEntry.Value - } - } - return -} - -func calculateNetwork(network map[string]types.NetworkStats) (float64, float64) { - var rx, tx float64 - - for _, v := range network { - rx += float64(v.RxBytes) - tx += float64(v.TxBytes) - } - return rx, tx -} diff --git a/cli/command/container/stats_unit_test.go b/cli/command/container/stats_unit_test.go deleted file mode 100644 index 612914c9cd..0000000000 --- a/cli/command/container/stats_unit_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package container - -import ( - "testing" - - "github.com/docker/docker/api/types" -) - -func TestCalculateBlockIO(t *testing.T) { - blkio := types.BlkioStats{ - IoServiceBytesRecursive: []types.BlkioStatEntry{{Major: 8, Minor: 0, Op: "read", Value: 1234}, {Major: 8, Minor: 1, Op: "read", Value: 4567}, {Major: 8, Minor: 0, Op: "write", Value: 123}, {Major: 8, Minor: 1, Op: "write", Value: 456}}, - } - blkRead, blkWrite := calculateBlockIO(blkio) - if blkRead != 5801 { - t.Fatalf("blkRead = %d, want 5801", blkRead) - } - if blkWrite != 579 { - t.Fatalf("blkWrite = %d, want 579", blkWrite) - } -} diff --git a/cli/command/container/stop.go b/cli/command/container/stop.go deleted file mode 100644 index 32729e1eae..0000000000 --- a/cli/command/container/stop.go +++ /dev/null @@ -1,67 +0,0 @@ -package container - -import ( - "fmt" - "strings" - "time" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type stopOptions struct { - time int - timeChanged bool - - containers []string -} - -// NewStopCommand creates a new cobra.Command for `docker stop` -func NewStopCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts stopOptions - - cmd := &cobra.Command{ - Use: "stop [OPTIONS] CONTAINER [CONTAINER...]", - Short: "Stop one or more running containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - opts.timeChanged = cmd.Flags().Changed("time") - return runStop(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.IntVarP(&opts.time, "time", "t", 10, "Seconds to wait for stop before killing it") - return cmd -} - -func runStop(dockerCli *command.DockerCli, opts *stopOptions) error { - ctx := context.Background() - - var timeout *time.Duration - if opts.timeChanged { - timeoutValue := time.Duration(opts.time) * time.Second - timeout = &timeoutValue - } - - var errs []string - - errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, id string) error { - return dockerCli.Client().ContainerStop(ctx, id, timeout) - }) - for _, container := range opts.containers { - if err := <-errChan; err != nil { - errs = append(errs, err.Error()) - continue - } - fmt.Fprintln(dockerCli.Out(), container) - } - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/container/testdata/utf16.env b/cli/command/container/testdata/utf16.env deleted file mode 100755 index 3a73358fff..0000000000 Binary files a/cli/command/container/testdata/utf16.env and /dev/null differ diff --git a/cli/command/container/testdata/utf16be.env b/cli/command/container/testdata/utf16be.env deleted file mode 100755 index e523da7af4..0000000000 Binary files a/cli/command/container/testdata/utf16be.env and /dev/null differ diff --git a/cli/command/container/testdata/utf8.env b/cli/command/container/testdata/utf8.env deleted file mode 100755 index 1ce45055b7..0000000000 --- a/cli/command/container/testdata/utf8.env +++ /dev/null @@ -1,3 +0,0 @@ -FOO=BAR -HELLO=您好 -BAR=FOO \ No newline at end of file diff --git a/cli/command/container/testdata/valid.env b/cli/command/container/testdata/valid.env deleted file mode 100644 index 3afbdc81c2..0000000000 --- a/cli/command/container/testdata/valid.env +++ /dev/null @@ -1 +0,0 @@ -ENV1=value1 diff --git a/cli/command/container/testdata/valid.label b/cli/command/container/testdata/valid.label deleted file mode 100644 index b4208bdf8f..0000000000 --- a/cli/command/container/testdata/valid.label +++ /dev/null @@ -1 +0,0 @@ -LABEL1=value1 diff --git a/cli/command/container/top.go b/cli/command/container/top.go deleted file mode 100644 index 4a6d3ed5cf..0000000000 --- a/cli/command/container/top.go +++ /dev/null @@ -1,57 +0,0 @@ -package container - -import ( - "fmt" - "strings" - "text/tabwriter" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type topOptions struct { - container string - - args []string -} - -// NewTopCommand creates a new cobra.Command for `docker top` -func NewTopCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts topOptions - - cmd := &cobra.Command{ - Use: "top CONTAINER [ps OPTIONS]", - Short: "Display the running processes of a container", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.container = args[0] - opts.args = args[1:] - return runTop(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.SetInterspersed(false) - - return cmd -} - -func runTop(dockerCli *command.DockerCli, opts *topOptions) error { - ctx := context.Background() - - procList, err := dockerCli.Client().ContainerTop(ctx, opts.container, opts.args) - if err != nil { - return err - } - - w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) - fmt.Fprintln(w, strings.Join(procList.Titles, "\t")) - - for _, proc := range procList.Processes { - fmt.Fprintln(w, strings.Join(proc, "\t")) - } - w.Flush() - return nil -} diff --git a/cli/command/container/tty.go b/cli/command/container/tty.go deleted file mode 100644 index 6af8e2becf..0000000000 --- a/cli/command/container/tty.go +++ /dev/null @@ -1,103 +0,0 @@ -package container - -import ( - "fmt" - "os" - gosignal "os/signal" - "runtime" - "time" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/signal" - "golang.org/x/net/context" -) - -// resizeTtyTo resizes tty to specific height and width -func resizeTtyTo(ctx context.Context, client client.ContainerAPIClient, id string, height, width uint, isExec bool) { - if height == 0 && width == 0 { - return - } - - options := types.ResizeOptions{ - Height: height, - Width: width, - } - - var err error - if isExec { - err = client.ContainerExecResize(ctx, id, options) - } else { - err = client.ContainerResize(ctx, id, options) - } - - if err != nil { - logrus.Debugf("Error resize: %s", err) - } -} - -// MonitorTtySize updates the container tty size when the terminal tty changes size -func MonitorTtySize(ctx context.Context, cli *command.DockerCli, id string, isExec bool) error { - resizeTty := func() { - height, width := cli.Out().GetTtySize() - resizeTtyTo(ctx, cli.Client(), id, height, width, isExec) - } - - resizeTty() - - if runtime.GOOS == "windows" { - go func() { - prevH, prevW := cli.Out().GetTtySize() - for { - time.Sleep(time.Millisecond * 250) - h, w := cli.Out().GetTtySize() - - if prevW != w || prevH != h { - resizeTty() - } - prevH = h - prevW = w - } - }() - } else { - sigchan := make(chan os.Signal, 1) - gosignal.Notify(sigchan, signal.SIGWINCH) - go func() { - for range sigchan { - resizeTty() - } - }() - } - return nil -} - -// ForwardAllSignals forwards signals to the container -func ForwardAllSignals(ctx context.Context, cli *command.DockerCli, cid string) chan os.Signal { - sigc := make(chan os.Signal, 128) - signal.CatchAll(sigc) - go func() { - for s := range sigc { - if s == signal.SIGCHLD || s == signal.SIGPIPE { - continue - } - var sig string - for sigStr, sigN := range signal.SignalMap { - if sigN == s { - sig = sigStr - break - } - } - if sig == "" { - fmt.Fprintf(cli.Err(), "Unsupported signal: %v. Discarding.\n", s) - continue - } - - if err := cli.Client().ContainerKill(ctx, cid, sig); err != nil { - logrus.Debugf("Error sending signal: %s", err) - } - } - }() - return sigc -} diff --git a/cli/command/container/unpause.go b/cli/command/container/unpause.go deleted file mode 100644 index 8105b17551..0000000000 --- a/cli/command/container/unpause.go +++ /dev/null @@ -1,50 +0,0 @@ -package container - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type unpauseOptions struct { - containers []string -} - -// NewUnpauseCommand creates a new cobra.Command for `docker unpause` -func NewUnpauseCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts unpauseOptions - - cmd := &cobra.Command{ - Use: "unpause CONTAINER [CONTAINER...]", - Short: "Unpause all processes within one or more containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - return runUnpause(dockerCli, &opts) - }, - } - return cmd -} - -func runUnpause(dockerCli *command.DockerCli, opts *unpauseOptions) error { - ctx := context.Background() - - var errs []string - errChan := parallelOperation(ctx, opts.containers, dockerCli.Client().ContainerUnpause) - for _, container := range opts.containers { - if err := <-errChan; err != nil { - errs = append(errs, err.Error()) - continue - } - fmt.Fprintln(dockerCli.Out(), container) - } - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/container/update.go b/cli/command/container/update.go deleted file mode 100644 index a650815e8e..0000000000 --- a/cli/command/container/update.go +++ /dev/null @@ -1,134 +0,0 @@ -package container - -import ( - "fmt" - "strings" - - containertypes "github.com/docker/docker/api/types/container" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type updateOptions struct { - blkioWeight uint16 - cpuPeriod int64 - cpuQuota int64 - cpuRealtimePeriod int64 - cpuRealtimeRuntime int64 - cpusetCpus string - cpusetMems string - cpuShares int64 - memory opts.MemBytes - memoryReservation opts.MemBytes - memorySwap opts.MemSwapBytes - kernelMemory opts.MemBytes - restartPolicy string - cpus opts.NanoCPUs - - nFlag int - - containers []string -} - -// NewUpdateCommand creates a new cobra.Command for `docker update` -func NewUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts updateOptions - - cmd := &cobra.Command{ - Use: "update [OPTIONS] CONTAINER [CONTAINER...]", - Short: "Update configuration of one or more containers", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - opts.nFlag = cmd.Flags().NFlag() - return runUpdate(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.Uint16Var(&opts.blkioWeight, "blkio-weight", 0, "Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)") - flags.Int64Var(&opts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") - flags.Int64Var(&opts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota") - flags.Int64Var(&opts.cpuRealtimePeriod, "cpu-rt-period", 0, "Limit the CPU real-time period in microseconds") - flags.SetAnnotation("cpu-rt-period", "version", []string{"1.25"}) - flags.Int64Var(&opts.cpuRealtimeRuntime, "cpu-rt-runtime", 0, "Limit the CPU real-time runtime in microseconds") - flags.SetAnnotation("cpu-rt-runtime", "version", []string{"1.25"}) - flags.StringVar(&opts.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") - flags.StringVar(&opts.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") - flags.Int64VarP(&opts.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") - flags.VarP(&opts.memory, "memory", "m", "Memory limit") - flags.Var(&opts.memoryReservation, "memory-reservation", "Memory soft limit") - flags.Var(&opts.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") - flags.Var(&opts.kernelMemory, "kernel-memory", "Kernel memory limit") - flags.StringVar(&opts.restartPolicy, "restart", "", "Restart policy to apply when a container exits") - - flags.Var(&opts.cpus, "cpus", "Number of CPUs") - flags.SetAnnotation("cpus", "version", []string{"1.29"}) - - return cmd -} - -func runUpdate(dockerCli *command.DockerCli, opts *updateOptions) error { - var err error - - if opts.nFlag == 0 { - return errors.New("You must provide one or more flags when using this command.") - } - - var restartPolicy containertypes.RestartPolicy - if opts.restartPolicy != "" { - restartPolicy, err = runconfigopts.ParseRestartPolicy(opts.restartPolicy) - if err != nil { - return err - } - } - - resources := containertypes.Resources{ - BlkioWeight: opts.blkioWeight, - CpusetCpus: opts.cpusetCpus, - CpusetMems: opts.cpusetMems, - CPUShares: opts.cpuShares, - Memory: opts.memory.Value(), - MemoryReservation: opts.memoryReservation.Value(), - MemorySwap: opts.memorySwap.Value(), - KernelMemory: opts.kernelMemory.Value(), - CPUPeriod: opts.cpuPeriod, - CPUQuota: opts.cpuQuota, - CPURealtimePeriod: opts.cpuRealtimePeriod, - CPURealtimeRuntime: opts.cpuRealtimeRuntime, - NanoCPUs: opts.cpus.Value(), - } - - updateConfig := containertypes.UpdateConfig{ - Resources: resources, - RestartPolicy: restartPolicy, - } - - ctx := context.Background() - - var ( - warns []string - errs []string - ) - for _, container := range opts.containers { - r, err := dockerCli.Client().ContainerUpdate(ctx, container, updateConfig) - if err != nil { - errs = append(errs, err.Error()) - } else { - fmt.Fprintln(dockerCli.Out(), container) - } - warns = append(warns, r.Warnings...) - } - if len(warns) > 0 { - fmt.Fprintln(dockerCli.Out(), strings.Join(warns, "\n")) - } - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/container/utils.go b/cli/command/container/utils.go deleted file mode 100644 index e4664b745c..0000000000 --- a/cli/command/container/utils.go +++ /dev/null @@ -1,142 +0,0 @@ -package container - -import ( - "strconv" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/events" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/cli/command" - clientapi "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -func waitExitOrRemoved(ctx context.Context, dockerCli *command.DockerCli, containerID string, waitRemove bool) chan int { - if len(containerID) == 0 { - // containerID can never be empty - panic("Internal Error: waitExitOrRemoved needs a containerID as parameter") - } - - var removeErr error - statusChan := make(chan int) - exitCode := 125 - - // Get events via Events API - f := filters.NewArgs() - f.Add("type", "container") - f.Add("container", containerID) - options := types.EventsOptions{ - Filters: f, - } - eventCtx, cancel := context.WithCancel(ctx) - eventq, errq := dockerCli.Client().Events(eventCtx, options) - - eventProcessor := func(e events.Message) bool { - stopProcessing := false - switch e.Status { - case "die": - if v, ok := e.Actor.Attributes["exitCode"]; ok { - code, cerr := strconv.Atoi(v) - if cerr != nil { - logrus.Errorf("failed to convert exitcode '%q' to int: %v", v, cerr) - } else { - exitCode = code - } - } - if !waitRemove { - stopProcessing = true - } else { - // If we are talking to an older daemon, `AutoRemove` is not supported. - // We need to fall back to the old behavior, which is client-side removal - if versions.LessThan(dockerCli.Client().ClientVersion(), "1.25") { - go func() { - removeErr = dockerCli.Client().ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{RemoveVolumes: true}) - if removeErr != nil { - logrus.Errorf("error removing container: %v", removeErr) - cancel() // cancel the event Q - } - }() - } - } - case "detach": - exitCode = 0 - stopProcessing = true - case "destroy": - stopProcessing = true - } - return stopProcessing - } - - go func() { - defer func() { - statusChan <- exitCode // must always send an exit code or the caller will block - cancel() - }() - - for { - select { - case <-eventCtx.Done(): - if removeErr != nil { - return - } - case evt := <-eventq: - if eventProcessor(evt) { - return - } - case err := <-errq: - logrus.Errorf("error getting events from daemon: %v", err) - return - } - } - }() - - return statusChan -} - -// getExitCode performs an inspect on the container. It returns -// the running state and the exit code. -func getExitCode(ctx context.Context, dockerCli *command.DockerCli, containerID string) (bool, int, error) { - c, err := dockerCli.Client().ContainerInspect(ctx, containerID) - if err != nil { - // If we can't connect, then the daemon probably died. - if !clientapi.IsErrConnectionFailed(err) { - return false, -1, err - } - return false, -1, nil - } - return c.State.Running, c.State.ExitCode, nil -} - -func parallelOperation(ctx context.Context, containers []string, op func(ctx context.Context, container string) error) chan error { - if len(containers) == 0 { - return nil - } - const defaultParallel int = 50 - sem := make(chan struct{}, defaultParallel) - errChan := make(chan error) - - // make sure result is printed in correct order - output := map[string]chan error{} - for _, c := range containers { - output[c] = make(chan error, 1) - } - go func() { - for _, c := range containers { - err := <-output[c] - errChan <- err - } - }() - - go func() { - for _, c := range containers { - sem <- struct{}{} // Wait for active queue sem to drain. - go func(container string) { - output[container] <- op(ctx, container) - <-sem - }(c) - } - }() - return errChan -} diff --git a/cli/command/container/wait.go b/cli/command/container/wait.go deleted file mode 100644 index f978207b94..0000000000 --- a/cli/command/container/wait.go +++ /dev/null @@ -1,50 +0,0 @@ -package container - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type waitOptions struct { - containers []string -} - -// NewWaitCommand creates a new cobra.Command for `docker wait` -func NewWaitCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts waitOptions - - cmd := &cobra.Command{ - Use: "wait CONTAINER [CONTAINER...]", - Short: "Block until one or more containers stop, then print their exit codes", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - return runWait(dockerCli, &opts) - }, - } - return cmd -} - -func runWait(dockerCli *command.DockerCli, opts *waitOptions) error { - ctx := context.Background() - - var errs []string - for _, container := range opts.containers { - status, err := dockerCli.Client().ContainerWait(ctx, container) - if err != nil { - errs = append(errs, err.Error()) - continue - } - fmt.Fprintf(dockerCli.Out(), "%d\n", status) - } - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/events_utils.go b/cli/command/events_utils.go deleted file mode 100644 index e710c97576..0000000000 --- a/cli/command/events_utils.go +++ /dev/null @@ -1,49 +0,0 @@ -package command - -import ( - "sync" - - "github.com/Sirupsen/logrus" - eventtypes "github.com/docker/docker/api/types/events" -) - -type eventProcessor func(eventtypes.Message, error) error - -// EventHandler is abstract interface for user to customize -// own handle functions of each type of events -type EventHandler interface { - Handle(action string, h func(eventtypes.Message)) - Watch(c <-chan eventtypes.Message) -} - -// InitEventHandler initializes and returns an EventHandler -func InitEventHandler() EventHandler { - return &eventHandler{handlers: make(map[string]func(eventtypes.Message))} -} - -type eventHandler struct { - handlers map[string]func(eventtypes.Message) - mu sync.Mutex -} - -func (w *eventHandler) Handle(action string, h func(eventtypes.Message)) { - w.mu.Lock() - w.handlers[action] = h - w.mu.Unlock() -} - -// Watch ranges over the passed in event chan and processes the events based on the -// handlers created for a given action. -// To stop watching, close the event chan. -func (w *eventHandler) Watch(c <-chan eventtypes.Message) { - for e := range c { - w.mu.Lock() - h, exists := w.handlers[e.Action] - w.mu.Unlock() - if !exists { - continue - } - logrus.Debugf("event handler: received event: %v", e) - go h(e) - } -} diff --git a/cli/command/formatter/checkpoint.go b/cli/command/formatter/checkpoint.go deleted file mode 100644 index 041fcafb7d..0000000000 --- a/cli/command/formatter/checkpoint.go +++ /dev/null @@ -1,52 +0,0 @@ -package formatter - -import "github.com/docker/docker/api/types" - -const ( - defaultCheckpointFormat = "table {{.Name}}" - - checkpointNameHeader = "CHECKPOINT NAME" -) - -// NewCheckpointFormat returns a format for use with a checkpoint Context -func NewCheckpointFormat(source string) Format { - switch source { - case TableFormatKey: - return defaultCheckpointFormat - } - return Format(source) -} - -// CheckpointWrite writes formatted checkpoints using the Context -func CheckpointWrite(ctx Context, checkpoints []types.Checkpoint) error { - render := func(format func(subContext subContext) error) error { - for _, checkpoint := range checkpoints { - if err := format(&checkpointContext{c: checkpoint}); err != nil { - return err - } - } - return nil - } - return ctx.Write(newCheckpointContext(), render) -} - -type checkpointContext struct { - HeaderContext - c types.Checkpoint -} - -func newCheckpointContext() *checkpointContext { - cpCtx := checkpointContext{} - cpCtx.header = volumeHeaderContext{ - "Name": checkpointNameHeader, - } - return &cpCtx -} - -func (c *checkpointContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *checkpointContext) Name() string { - return c.c.Name -} diff --git a/cli/command/formatter/checkpoint_test.go b/cli/command/formatter/checkpoint_test.go deleted file mode 100644 index e88c4d0132..0000000000 --- a/cli/command/formatter/checkpoint_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package formatter - -import ( - "bytes" - "testing" - - "github.com/docker/docker/api/types" - "github.com/stretchr/testify/assert" -) - -func TestCheckpointContextFormatWrite(t *testing.T) { - cases := []struct { - context Context - expected string - }{ - { - Context{Format: NewCheckpointFormat(defaultCheckpointFormat)}, - `CHECKPOINT NAME -checkpoint-1 -checkpoint-2 -checkpoint-3 -`, - }, - { - Context{Format: NewCheckpointFormat("{{.Name}}")}, - `checkpoint-1 -checkpoint-2 -checkpoint-3 -`, - }, - { - Context{Format: NewCheckpointFormat("{{.Name}}:")}, - `checkpoint-1: -checkpoint-2: -checkpoint-3: -`, - }, - } - - checkpoints := []types.Checkpoint{ - {"checkpoint-1"}, - {"checkpoint-2"}, - {"checkpoint-3"}, - } - for _, testcase := range cases { - out := bytes.NewBufferString("") - testcase.context.Output = out - err := CheckpointWrite(testcase.context, checkpoints) - if err != nil { - assert.Error(t, err, testcase.expected) - } else { - assert.Equal(t, out.String(), testcase.expected) - } - } -} diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go deleted file mode 100644 index 9b5c24636c..0000000000 --- a/cli/command/formatter/container.go +++ /dev/null @@ -1,259 +0,0 @@ -package formatter - -import ( - "fmt" - "strconv" - "strings" - "time" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api" - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/stringutils" - units "github.com/docker/go-units" -) - -const ( - defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}" - - containerIDHeader = "CONTAINER ID" - namesHeader = "NAMES" - commandHeader = "COMMAND" - runningForHeader = "CREATED" - statusHeader = "STATUS" - portsHeader = "PORTS" - mountsHeader = "MOUNTS" - localVolumes = "LOCAL VOLUMES" - networksHeader = "NETWORKS" -) - -// NewContainerFormat returns a Format for rendering using a Context -func NewContainerFormat(source string, quiet bool, size bool) Format { - switch source { - case TableFormatKey: - if quiet { - return defaultQuietFormat - } - format := defaultContainerTableFormat - if size { - format += `\t{{.Size}}` - } - return Format(format) - case RawFormatKey: - if quiet { - return `container_id: {{.ID}}` - } - format := `container_id: {{.ID}} -image: {{.Image}} -command: {{.Command}} -created_at: {{.CreatedAt}} -status: {{- pad .Status 1 0}} -names: {{.Names}} -labels: {{- pad .Labels 1 0}} -ports: {{- pad .Ports 1 0}} -` - if size { - format += `size: {{.Size}}\n` - } - return Format(format) - } - return Format(source) -} - -// ContainerWrite renders the context for a list of containers -func ContainerWrite(ctx Context, containers []types.Container) error { - render := func(format func(subContext subContext) error) error { - for _, container := range containers { - err := format(&containerContext{trunc: ctx.Trunc, c: container}) - if err != nil { - return err - } - } - return nil - } - return ctx.Write(newContainerContext(), render) -} - -type containerHeaderContext map[string]string - -func (c containerHeaderContext) Label(name string) string { - n := strings.Split(name, ".") - r := strings.NewReplacer("-", " ", "_", " ") - h := r.Replace(n[len(n)-1]) - - return h -} - -type containerContext struct { - HeaderContext - trunc bool - c types.Container -} - -func newContainerContext() *containerContext { - containerCtx := containerContext{} - containerCtx.header = containerHeaderContext{ - "ID": containerIDHeader, - "Names": namesHeader, - "Image": imageHeader, - "Command": commandHeader, - "CreatedAt": createdAtHeader, - "RunningFor": runningForHeader, - "Ports": portsHeader, - "Status": statusHeader, - "Size": sizeHeader, - "Labels": labelsHeader, - "Mounts": mountsHeader, - "LocalVolumes": localVolumes, - "Networks": networksHeader, - } - return &containerCtx -} - -func (c *containerContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *containerContext) ID() string { - if c.trunc { - return stringid.TruncateID(c.c.ID) - } - return c.c.ID -} - -func (c *containerContext) Names() string { - names := stripNamePrefix(c.c.Names) - if c.trunc { - for _, name := range names { - if len(strings.Split(name, "/")) == 1 { - names = []string{name} - break - } - } - } - return strings.Join(names, ",") -} - -func (c *containerContext) Image() string { - if c.c.Image == "" { - return "" - } - if c.trunc { - if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) { - return trunc - } - // truncate digest if no-trunc option was not selected - ref, err := reference.ParseNormalizedNamed(c.c.Image) - if err == nil { - if nt, ok := ref.(reference.NamedTagged); ok { - // case for when a tag is provided - if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil { - return reference.FamiliarString(namedTagged) - } - } else { - // case for when a tag is not provided - named := reference.TrimNamed(ref) - return reference.FamiliarString(named) - } - } - } - - return c.c.Image -} - -func (c *containerContext) Command() string { - command := c.c.Command - if c.trunc { - command = stringutils.Ellipsis(command, 20) - } - return strconv.Quote(command) -} - -func (c *containerContext) CreatedAt() string { - return time.Unix(int64(c.c.Created), 0).String() -} - -func (c *containerContext) RunningFor() string { - createdAt := time.Unix(int64(c.c.Created), 0) - return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago" -} - -func (c *containerContext) Ports() string { - return api.DisplayablePorts(c.c.Ports) -} - -func (c *containerContext) Status() string { - return c.c.Status -} - -func (c *containerContext) Size() string { - srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3) - sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3) - - sf := srw - if c.c.SizeRootFs > 0 { - sf = fmt.Sprintf("%s (virtual %s)", srw, sv) - } - return sf -} - -func (c *containerContext) Labels() string { - if c.c.Labels == nil { - return "" - } - - var joinLabels []string - for k, v := range c.c.Labels { - joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) - } - return strings.Join(joinLabels, ",") -} - -func (c *containerContext) Label(name string) string { - if c.c.Labels == nil { - return "" - } - return c.c.Labels[name] -} - -func (c *containerContext) Mounts() string { - var name string - var mounts []string - for _, m := range c.c.Mounts { - if m.Name == "" { - name = m.Source - } else { - name = m.Name - } - if c.trunc { - name = stringutils.Ellipsis(name, 15) - } - mounts = append(mounts, name) - } - return strings.Join(mounts, ",") -} - -func (c *containerContext) LocalVolumes() string { - count := 0 - for _, m := range c.c.Mounts { - if m.Driver == "local" { - count++ - } - } - - return fmt.Sprintf("%d", count) -} - -func (c *containerContext) Networks() string { - if c.c.NetworkSettings == nil { - return "" - } - - networks := []string{} - for k := range c.c.NetworkSettings.Networks { - networks = append(networks, k) - } - - return strings.Join(networks, ",") -} diff --git a/cli/command/formatter/container_test.go b/cli/command/formatter/container_test.go deleted file mode 100644 index 8d23cc781c..0000000000 --- a/cli/command/formatter/container_test.go +++ /dev/null @@ -1,385 +0,0 @@ -package formatter - -import ( - "bytes" - "encoding/json" - "fmt" - "strings" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" - "github.com/stretchr/testify/assert" -) - -func TestContainerPsContext(t *testing.T) { - containerID := stringid.GenerateRandomID() - unix := time.Now().Add(-65 * time.Second).Unix() - - var ctx containerContext - cases := []struct { - container types.Container - trunc bool - expValue string - call func() string - }{ - {types.Container{ID: containerID}, true, stringid.TruncateID(containerID), ctx.ID}, - {types.Container{ID: containerID}, false, containerID, ctx.ID}, - {types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", ctx.Names}, - {types.Container{Image: "ubuntu"}, true, "ubuntu", ctx.Image}, - {types.Container{Image: "verylongimagename"}, true, "verylongimagename", ctx.Image}, - {types.Container{Image: "verylongimagename"}, false, "verylongimagename", ctx.Image}, - {types.Container{ - Image: "a5a665ff33eced1e0803148700880edab4", - ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", - }, - true, - "a5a665ff33ec", - ctx.Image, - }, - {types.Container{ - Image: "a5a665ff33eced1e0803148700880edab4", - ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", - }, - false, - "a5a665ff33eced1e0803148700880edab4", - ctx.Image, - }, - {types.Container{Image: ""}, true, "", ctx.Image}, - {types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, ctx.Command}, - {types.Container{Created: unix}, true, time.Unix(unix, 0).String(), ctx.CreatedAt}, - {types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", ctx.Ports}, - {types.Container{Status: "RUNNING"}, true, "RUNNING", ctx.Status}, - {types.Container{SizeRw: 10}, true, "10B", ctx.Size}, - {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10B (virtual 20B)", ctx.Size}, - {types.Container{}, true, "", ctx.Labels}, - {types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", ctx.Labels}, - {types.Container{Created: unix}, true, "About a minute ago", ctx.RunningFor}, - {types.Container{ - Mounts: []types.MountPoint{ - { - Name: "this-is-a-long-volume-name-and-will-be-truncated-if-trunc-is-set", - Driver: "local", - Source: "/a/path", - }, - }, - }, true, "this-is-a-lo...", ctx.Mounts}, - {types.Container{ - Mounts: []types.MountPoint{ - { - Driver: "local", - Source: "/a/path", - }, - }, - }, false, "/a/path", ctx.Mounts}, - {types.Container{ - Mounts: []types.MountPoint{ - { - Name: "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", - Driver: "local", - Source: "/a/path", - }, - }, - }, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", ctx.Mounts}, - } - - for _, c := range cases { - ctx = containerContext{c: c.container, trunc: c.trunc} - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } - - c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}} - ctx = containerContext{c: c1, trunc: true} - - sid := ctx.Label("com.docker.swarm.swarm-id") - node := ctx.Label("com.docker.swarm.node_name") - if sid != "33" { - t.Fatalf("Expected 33, was %s\n", sid) - } - - if node != "ubuntu" { - t.Fatalf("Expected ubuntu, was %s\n", node) - } - - c2 := types.Container{} - ctx = containerContext{c: c2, trunc: true} - - label := ctx.Label("anything.really") - if label != "" { - t.Fatalf("Expected an empty string, was %s", label) - } -} - -func TestContainerContextWrite(t *testing.T) { - unixTime := time.Now().AddDate(0, 0, -1).Unix() - expectedTime := time.Unix(unixTime, 0).String() - - cases := []struct { - context Context - expected string - }{ - // Errors - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table Format - { - Context{Format: NewContainerFormat("table", false, true)}, - `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE -containerID1 ubuntu "" 24 hours ago foobar_baz 0B -containerID2 ubuntu "" 24 hours ago foobar_bar 0B -`, - }, - { - Context{Format: NewContainerFormat("table", false, false)}, - `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -containerID1 ubuntu "" 24 hours ago foobar_baz -containerID2 ubuntu "" 24 hours ago foobar_bar -`, - }, - { - Context{Format: NewContainerFormat("table {{.Image}}", false, false)}, - "IMAGE\nubuntu\nubuntu\n", - }, - { - Context{Format: NewContainerFormat("table {{.Image}}", false, true)}, - "IMAGE\nubuntu\nubuntu\n", - }, - { - Context{Format: NewContainerFormat("table {{.Image}}", true, false)}, - "IMAGE\nubuntu\nubuntu\n", - }, - { - Context{Format: NewContainerFormat("table", true, false)}, - "containerID1\ncontainerID2\n", - }, - // Raw Format - { - Context{Format: NewContainerFormat("raw", false, false)}, - fmt.Sprintf(`container_id: containerID1 -image: ubuntu -command: "" -created_at: %s -status: -names: foobar_baz -labels: -ports: - -container_id: containerID2 -image: ubuntu -command: "" -created_at: %s -status: -names: foobar_bar -labels: -ports: - -`, expectedTime, expectedTime), - }, - { - Context{Format: NewContainerFormat("raw", false, true)}, - fmt.Sprintf(`container_id: containerID1 -image: ubuntu -command: "" -created_at: %s -status: -names: foobar_baz -labels: -ports: -size: 0B - -container_id: containerID2 -image: ubuntu -command: "" -created_at: %s -status: -names: foobar_bar -labels: -ports: -size: 0B - -`, expectedTime, expectedTime), - }, - { - Context{Format: NewContainerFormat("raw", true, false)}, - "container_id: containerID1\ncontainer_id: containerID2\n", - }, - // Custom Format - { - Context{Format: "{{.Image}}"}, - "ubuntu\nubuntu\n", - }, - { - Context{Format: NewContainerFormat("{{.Image}}", false, true)}, - "ubuntu\nubuntu\n", - }, - // Special headers for customerized table format - { - Context{Format: NewContainerFormat(`table {{truncate .ID 5}}\t{{json .Image}} {{.RunningFor}}/{{title .Status}}/{{pad .Ports 2 2}}.{{upper .Names}} {{lower .Status}}`, false, true)}, - `CONTAINER ID IMAGE CREATED/STATUS/ PORTS .NAMES STATUS -conta "ubuntu" 24 hours ago//.FOOBAR_BAZ -conta "ubuntu" 24 hours ago//.FOOBAR_BAR -`, - }, - } - - for _, testcase := range cases { - containers := []types.Container{ - {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime}, - {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime}, - } - out := bytes.NewBufferString("") - testcase.context.Output = out - err := ContainerWrite(testcase.context, containers) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} - -func TestContainerContextWriteWithNoContainers(t *testing.T) { - out := bytes.NewBufferString("") - containers := []types.Container{} - - contexts := []struct { - context Context - expected string - }{ - { - Context{ - Format: "{{.Image}}", - Output: out, - }, - "", - }, - { - Context{ - Format: "table {{.Image}}", - Output: out, - }, - "IMAGE\n", - }, - { - Context{ - Format: NewContainerFormat("{{.Image}}", false, true), - Output: out, - }, - "", - }, - { - Context{ - Format: NewContainerFormat("table {{.Image}}", false, true), - Output: out, - }, - "IMAGE\n", - }, - { - Context{ - Format: "table {{.Image}}\t{{.Size}}", - Output: out, - }, - "IMAGE SIZE\n", - }, - { - Context{ - Format: NewContainerFormat("table {{.Image}}\t{{.Size}}", false, true), - Output: out, - }, - "IMAGE SIZE\n", - }, - } - - for _, context := range contexts { - ContainerWrite(context.context, containers) - assert.Equal(t, context.expected, out.String()) - // Clean buffer - out.Reset() - } -} - -func TestContainerContextWriteJSON(t *testing.T) { - unix := time.Now().Add(-65 * time.Second).Unix() - containers := []types.Container{ - {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unix}, - {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unix}, - } - expectedCreated := time.Unix(unix, 0).String() - expectedJSONs := []map[string]interface{}{ - {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Networks": "", "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", "Status": ""}, - {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Networks": "", "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", "Status": ""}, - } - out := bytes.NewBufferString("") - err := ContainerWrite(Context{Format: "{{json .}}", Output: out}, containers) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var m map[string]interface{} - if err := json.Unmarshal([]byte(line), &m); err != nil { - t.Fatal(err) - } - assert.Equal(t, expectedJSONs[i], m) - } -} - -func TestContainerContextWriteJSONField(t *testing.T) { - containers := []types.Container{ - {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu"}, - {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu"}, - } - out := bytes.NewBufferString("") - err := ContainerWrite(Context{Format: "{{json .ID}}", Output: out}, containers) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var s string - if err := json.Unmarshal([]byte(line), &s); err != nil { - t.Fatal(err) - } - assert.Equal(t, containers[i].ID, s) - } -} - -func TestContainerBackCompat(t *testing.T) { - containers := []types.Container{{ID: "brewhaha"}} - cases := []string{ - "ID", - "Names", - "Image", - "Command", - "CreatedAt", - "RunningFor", - "Ports", - "Status", - "Size", - "Labels", - "Mounts", - } - buf := bytes.NewBuffer(nil) - for _, c := range cases { - ctx := Context{Format: Format(fmt.Sprintf("{{ .%s }}", c)), Output: buf} - if err := ContainerWrite(ctx, containers); err != nil { - t.Logf("could not render template for field '%s': %v", c, err) - t.Fail() - } - buf.Reset() - } -} diff --git a/cli/command/formatter/custom.go b/cli/command/formatter/custom.go deleted file mode 100644 index 73487f63ef..0000000000 --- a/cli/command/formatter/custom.go +++ /dev/null @@ -1,35 +0,0 @@ -package formatter - -const ( - imageHeader = "IMAGE" - createdSinceHeader = "CREATED" - createdAtHeader = "CREATED AT" - sizeHeader = "SIZE" - labelsHeader = "LABELS" - nameHeader = "NAME" - driverHeader = "DRIVER" - scopeHeader = "SCOPE" -) - -type subContext interface { - FullHeader() interface{} -} - -// HeaderContext provides the subContext interface for managing headers -type HeaderContext struct { - header interface{} -} - -// FullHeader returns the header as an interface -func (c *HeaderContext) FullHeader() interface{} { - return c.header -} - -func stripNamePrefix(ss []string) []string { - sss := make([]string, len(ss)) - for i, s := range ss { - sss[i] = s[1:] - } - - return sss -} diff --git a/cli/command/formatter/custom_test.go b/cli/command/formatter/custom_test.go deleted file mode 100644 index da42039dca..0000000000 --- a/cli/command/formatter/custom_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package formatter - -import ( - "reflect" - "strings" - "testing" -) - -func compareMultipleValues(t *testing.T, value, expected string) { - // comma-separated values means probably a map input, which won't - // be guaranteed to have the same order as our expected value - // We'll create maps and use reflect.DeepEquals to check instead: - entriesMap := make(map[string]string) - expMap := make(map[string]string) - entries := strings.Split(value, ",") - expectedEntries := strings.Split(expected, ",") - for _, entry := range entries { - keyval := strings.Split(entry, "=") - entriesMap[keyval[0]] = keyval[1] - } - for _, expected := range expectedEntries { - keyval := strings.Split(expected, "=") - expMap[keyval[0]] = keyval[1] - } - if !reflect.DeepEqual(expMap, entriesMap) { - t.Fatalf("Expected entries: %v, got: %v", expected, value) - } -} diff --git a/cli/command/formatter/diff.go b/cli/command/formatter/diff.go deleted file mode 100644 index 9b4681934c..0000000000 --- a/cli/command/formatter/diff.go +++ /dev/null @@ -1,72 +0,0 @@ -package formatter - -import ( - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/pkg/archive" -) - -const ( - defaultDiffTableFormat = "table {{.Type}}\t{{.Path}}" - - changeTypeHeader = "CHANGE TYPE" - pathHeader = "PATH" -) - -// NewDiffFormat returns a format for use with a diff Context -func NewDiffFormat(source string) Format { - switch source { - case TableFormatKey: - return defaultDiffTableFormat - } - return Format(source) -} - -// DiffWrite writes formatted diff using the Context -func DiffWrite(ctx Context, changes []container.ContainerChangeResponseItem) error { - - render := func(format func(subContext subContext) error) error { - for _, change := range changes { - if err := format(&diffContext{c: change}); err != nil { - return err - } - } - return nil - } - return ctx.Write(newDiffContext(), render) -} - -type diffContext struct { - HeaderContext - c container.ContainerChangeResponseItem -} - -func newDiffContext() *diffContext { - diffCtx := diffContext{} - diffCtx.header = map[string]string{ - "Type": changeTypeHeader, - "Path": pathHeader, - } - return &diffCtx -} - -func (d *diffContext) MarshalJSON() ([]byte, error) { - return marshalJSON(d) -} - -func (d *diffContext) Type() string { - var kind string - switch d.c.Kind { - case archive.ChangeModify: - kind = "C" - case archive.ChangeAdd: - kind = "A" - case archive.ChangeDelete: - kind = "D" - } - return kind - -} - -func (d *diffContext) Path() string { - return d.c.Path -} diff --git a/cli/command/formatter/diff_test.go b/cli/command/formatter/diff_test.go deleted file mode 100644 index 1aa7b53056..0000000000 --- a/cli/command/formatter/diff_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package formatter - -import ( - "bytes" - "testing" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/pkg/archive" - "github.com/stretchr/testify/assert" -) - -func TestDiffContextFormatWrite(t *testing.T) { - // Check default output format (verbose and non-verbose mode) for table headers - cases := []struct { - context Context - expected string - }{ - { - Context{Format: NewDiffFormat("table")}, - `CHANGE TYPE PATH -C /var/log/app.log -A /usr/app/app.js -D /usr/app/old_app.js -`, - }, - { - Context{Format: NewDiffFormat("table {{.Path}}")}, - `PATH -/var/log/app.log -/usr/app/app.js -/usr/app/old_app.js -`, - }, - { - Context{Format: NewDiffFormat("{{.Type}}: {{.Path}}")}, - `C: /var/log/app.log -A: /usr/app/app.js -D: /usr/app/old_app.js -`, - }, - } - - diffs := []container.ContainerChangeResponseItem{ - {archive.ChangeModify, "/var/log/app.log"}, - {archive.ChangeAdd, "/usr/app/app.js"}, - {archive.ChangeDelete, "/usr/app/old_app.js"}, - } - - for _, testcase := range cases { - out := bytes.NewBufferString("") - testcase.context.Output = out - err := DiffWrite(testcase.context, diffs) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} diff --git a/cli/command/formatter/disk_usage.go b/cli/command/formatter/disk_usage.go deleted file mode 100644 index 07e39826ed..0000000000 --- a/cli/command/formatter/disk_usage.go +++ /dev/null @@ -1,358 +0,0 @@ -package formatter - -import ( - "bytes" - "fmt" - "strings" - "text/template" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - units "github.com/docker/go-units" -) - -const ( - defaultDiskUsageImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.VirtualSize}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}" - defaultDiskUsageContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Names}}" - defaultDiskUsageVolumeTableFormat = "table {{.Name}}\t{{.Links}}\t{{.Size}}" - defaultDiskUsageTableFormat = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}" - - typeHeader = "TYPE" - totalHeader = "TOTAL" - activeHeader = "ACTIVE" - reclaimableHeader = "RECLAIMABLE" - containersHeader = "CONTAINERS" - sharedSizeHeader = "SHARED SIZE" - uniqueSizeHeader = "UNIQUE SiZE" -) - -// DiskUsageContext contains disk usage specific information required by the formatter, encapsulate a Context struct. -type DiskUsageContext struct { - Context - Verbose bool - LayersSize int64 - Images []*types.ImageSummary - Containers []*types.Container - Volumes []*types.Volume -} - -func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template, error) { - ctx.buffer = bytes.NewBufferString("") - ctx.header = "" - ctx.Format = Format(format) - ctx.preFormat() - - return ctx.parseFormat() -} - -// -// NewDiskUsageFormat returns a format for rendering an DiskUsageContext -func NewDiskUsageFormat(source string) Format { - switch source { - case TableFormatKey: - format := defaultDiskUsageTableFormat - return Format(format) - case RawFormatKey: - format := `type: {{.Type}} -total: {{.TotalCount}} -active: {{.Active}} -size: {{.Size}} -reclaimable: {{.Reclaimable}} -` - return Format(format) - } - return Format(source) -} - -func (ctx *DiskUsageContext) Write() (err error) { - if ctx.Verbose == false { - ctx.buffer = bytes.NewBufferString("") - ctx.preFormat() - - tmpl, err := ctx.parseFormat() - if err != nil { - return err - } - - err = ctx.contextFormat(tmpl, &diskUsageImagesContext{ - totalSize: ctx.LayersSize, - images: ctx.Images, - }) - if err != nil { - return err - } - err = ctx.contextFormat(tmpl, &diskUsageContainersContext{ - containers: ctx.Containers, - }) - if err != nil { - return err - } - - err = ctx.contextFormat(tmpl, &diskUsageVolumesContext{ - volumes: ctx.Volumes, - }) - if err != nil { - return err - } - - diskUsageContainersCtx := diskUsageContainersContext{containers: []*types.Container{}} - diskUsageContainersCtx.header = map[string]string{ - "Type": typeHeader, - "TotalCount": totalHeader, - "Active": activeHeader, - "Size": sizeHeader, - "Reclaimable": reclaimableHeader, - } - ctx.postFormat(tmpl, &diskUsageContainersCtx) - - return err - } - - // First images - tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat) - if err != nil { - return - } - - ctx.Output.Write([]byte("Images space usage:\n\n")) - for _, i := range ctx.Images { - repo := "" - tag := "" - if len(i.RepoTags) > 0 && !isDangling(*i) { - // Only show the first tag - ref, err := reference.ParseNormalizedNamed(i.RepoTags[0]) - if err != nil { - continue - } - if nt, ok := ref.(reference.NamedTagged); ok { - repo = reference.FamiliarName(ref) - tag = nt.Tag() - } - } - - err = ctx.contextFormat(tmpl, &imageContext{ - repo: repo, - tag: tag, - trunc: true, - i: *i, - }) - if err != nil { - return - } - } - ctx.postFormat(tmpl, newImageContext()) - - // Now containers - ctx.Output.Write([]byte("\nContainers space usage:\n\n")) - tmpl, err = ctx.startSubsection(defaultDiskUsageContainerTableFormat) - if err != nil { - return - } - for _, c := range ctx.Containers { - // Don't display the virtual size - c.SizeRootFs = 0 - err = ctx.contextFormat(tmpl, &containerContext{ - trunc: true, - c: *c, - }) - if err != nil { - return - } - } - ctx.postFormat(tmpl, newContainerContext()) - - // And volumes - ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n")) - tmpl, err = ctx.startSubsection(defaultDiskUsageVolumeTableFormat) - if err != nil { - return - } - for _, v := range ctx.Volumes { - err = ctx.contextFormat(tmpl, &volumeContext{ - v: *v, - }) - if err != nil { - return - } - } - ctx.postFormat(tmpl, newVolumeContext()) - return -} - -type diskUsageImagesContext struct { - HeaderContext - totalSize int64 - images []*types.ImageSummary -} - -func (c *diskUsageImagesContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *diskUsageImagesContext) Type() string { - return "Images" -} - -func (c *diskUsageImagesContext) TotalCount() string { - return fmt.Sprintf("%d", len(c.images)) -} - -func (c *diskUsageImagesContext) Active() string { - used := 0 - for _, i := range c.images { - if i.Containers > 0 { - used++ - } - } - - return fmt.Sprintf("%d", used) -} - -func (c *diskUsageImagesContext) Size() string { - return units.HumanSize(float64(c.totalSize)) - -} - -func (c *diskUsageImagesContext) Reclaimable() string { - var used int64 - - for _, i := range c.images { - if i.Containers != 0 { - if i.VirtualSize == -1 || i.SharedSize == -1 { - continue - } - used += i.VirtualSize - i.SharedSize - } - } - - reclaimable := c.totalSize - used - if c.totalSize > 0 { - return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/c.totalSize) - } - return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable))) -} - -type diskUsageContainersContext struct { - HeaderContext - verbose bool - containers []*types.Container -} - -func (c *diskUsageContainersContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *diskUsageContainersContext) Type() string { - return "Containers" -} - -func (c *diskUsageContainersContext) TotalCount() string { - return fmt.Sprintf("%d", len(c.containers)) -} - -func (c *diskUsageContainersContext) isActive(container types.Container) bool { - return strings.Contains(container.State, "running") || - strings.Contains(container.State, "paused") || - strings.Contains(container.State, "restarting") -} - -func (c *diskUsageContainersContext) Active() string { - used := 0 - for _, container := range c.containers { - if c.isActive(*container) { - used++ - } - } - - return fmt.Sprintf("%d", used) -} - -func (c *diskUsageContainersContext) Size() string { - var size int64 - - for _, container := range c.containers { - size += container.SizeRw - } - - return units.HumanSize(float64(size)) -} - -func (c *diskUsageContainersContext) Reclaimable() string { - var reclaimable int64 - var totalSize int64 - - for _, container := range c.containers { - if !c.isActive(*container) { - reclaimable += container.SizeRw - } - totalSize += container.SizeRw - } - - if totalSize > 0 { - return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize) - } - - return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable))) -} - -type diskUsageVolumesContext struct { - HeaderContext - verbose bool - volumes []*types.Volume -} - -func (c *diskUsageVolumesContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *diskUsageVolumesContext) Type() string { - return "Local Volumes" -} - -func (c *diskUsageVolumesContext) TotalCount() string { - return fmt.Sprintf("%d", len(c.volumes)) -} - -func (c *diskUsageVolumesContext) Active() string { - - used := 0 - for _, v := range c.volumes { - if v.UsageData.RefCount > 0 { - used++ - } - } - - return fmt.Sprintf("%d", used) -} - -func (c *diskUsageVolumesContext) Size() string { - var size int64 - - for _, v := range c.volumes { - if v.UsageData.Size != -1 { - size += v.UsageData.Size - } - } - - return units.HumanSize(float64(size)) -} - -func (c *diskUsageVolumesContext) Reclaimable() string { - var reclaimable int64 - var totalSize int64 - - for _, v := range c.volumes { - if v.UsageData.Size != -1 { - if v.UsageData.RefCount == 0 { - reclaimable += v.UsageData.Size - } - totalSize += v.UsageData.Size - } - } - - if totalSize > 0 { - return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize) - } - - return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable))) -} diff --git a/cli/command/formatter/disk_usage_test.go b/cli/command/formatter/disk_usage_test.go deleted file mode 100644 index 302eb2c8f7..0000000000 --- a/cli/command/formatter/disk_usage_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package formatter - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestDiskUsageContextFormatWrite(t *testing.T) { - cases := []struct { - context DiskUsageContext - expected string - }{ - // Check default output format (verbose and non-verbose mode) for table headers - { - DiskUsageContext{ - Context: Context{ - Format: NewDiskUsageFormat("table"), - }, - Verbose: false}, - `TYPE TOTAL ACTIVE SIZE RECLAIMABLE -Images 0 0 0B 0B -Containers 0 0 0B 0B -Local Volumes 0 0 0B 0B -`, - }, - { - DiskUsageContext{Verbose: true}, - `Images space usage: - -REPOSITORY TAG IMAGE ID CREATED ago SIZE SHARED SIZE UNIQUE SiZE CONTAINERS - -Containers space usage: - -CONTAINER ID IMAGE COMMAND LOCAL VOLUMES SIZE CREATED ago STATUS NAMES - -Local Volumes space usage: - -VOLUME NAME LINKS SIZE -`, - }, - // Errors - { - DiskUsageContext{ - Context: Context{ - Format: "{{InvalidFunction}}", - }, - }, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - DiskUsageContext{ - Context: Context{ - Format: "{{nil}}", - }, - }, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table Format - { - DiskUsageContext{ - Context: Context{ - Format: NewDiskUsageFormat("table"), - }, - }, - `TYPE TOTAL ACTIVE SIZE RECLAIMABLE -Images 0 0 0B 0B -Containers 0 0 0B 0B -Local Volumes 0 0 0B 0B -`, - }, - { - DiskUsageContext{ - Context: Context{ - Format: NewDiskUsageFormat("table {{.Type}}\t{{.Active}}"), - }, - }, - `TYPE ACTIVE -Images 0 -Containers 0 -Local Volumes 0 -`, - }, - // Raw Format - { - DiskUsageContext{ - Context: Context{ - Format: NewDiskUsageFormat("raw"), - }, - }, - `type: Images -total: 0 -active: 0 -size: 0B -reclaimable: 0B - -type: Containers -total: 0 -active: 0 -size: 0B -reclaimable: 0B - -type: Local Volumes -total: 0 -active: 0 -size: 0B -reclaimable: 0B - -`, - }, - } - - for _, testcase := range cases { - out := bytes.NewBufferString("") - testcase.context.Output = out - if err := testcase.context.Write(); err != nil { - assert.Equal(t, testcase.expected, err.Error()) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} diff --git a/cli/command/formatter/formatter.go b/cli/command/formatter/formatter.go deleted file mode 100644 index 3f07aee963..0000000000 --- a/cli/command/formatter/formatter.go +++ /dev/null @@ -1,119 +0,0 @@ -package formatter - -import ( - "bytes" - "io" - "strings" - "text/tabwriter" - "text/template" - - "github.com/docker/docker/pkg/templates" - "github.com/pkg/errors" -) - -// Format keys used to specify certain kinds of output formats -const ( - TableFormatKey = "table" - RawFormatKey = "raw" - PrettyFormatKey = "pretty" - - defaultQuietFormat = "{{.ID}}" -) - -// Format is the format string rendered using the Context -type Format string - -// IsTable returns true if the format is a table-type format -func (f Format) IsTable() bool { - return strings.HasPrefix(string(f), TableFormatKey) -} - -// Contains returns true if the format contains the substring -func (f Format) Contains(sub string) bool { - return strings.Contains(string(f), sub) -} - -// Context contains information required by the formatter to print the output as desired. -type Context struct { - // Output is the output stream to which the formatted string is written. - Output io.Writer - // Format is used to choose raw, table or custom format for the output. - Format Format - // Trunc when set to true will truncate the output of certain fields such as Container ID. - Trunc bool - - // internal element - finalFormat string - header interface{} - buffer *bytes.Buffer -} - -func (c *Context) preFormat() { - c.finalFormat = string(c.Format) - - // TODO: handle this in the Format type - if c.Format.IsTable() { - c.finalFormat = c.finalFormat[len(TableFormatKey):] - } - - c.finalFormat = strings.Trim(c.finalFormat, " ") - r := strings.NewReplacer(`\t`, "\t", `\n`, "\n") - c.finalFormat = r.Replace(c.finalFormat) -} - -func (c *Context) parseFormat() (*template.Template, error) { - tmpl, err := templates.Parse(c.finalFormat) - if err != nil { - return tmpl, errors.Errorf("Template parsing error: %v\n", err) - } - return tmpl, err -} - -func (c *Context) postFormat(tmpl *template.Template, subContext subContext) { - if c.Format.IsTable() { - t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0) - buffer := bytes.NewBufferString("") - tmpl.Funcs(templates.HeaderFunctions).Execute(buffer, subContext.FullHeader()) - buffer.WriteTo(t) - t.Write([]byte("\n")) - c.buffer.WriteTo(t) - t.Flush() - } else { - c.buffer.WriteTo(c.Output) - } -} - -func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error { - if err := tmpl.Execute(c.buffer, subContext); err != nil { - return errors.Errorf("Template parsing error: %v\n", err) - } - if c.Format.IsTable() && c.header != nil { - c.header = subContext.FullHeader() - } - c.buffer.WriteString("\n") - return nil -} - -// SubFormat is a function type accepted by Write() -type SubFormat func(func(subContext) error) error - -// Write the template to the buffer using this Context -func (c *Context) Write(sub subContext, f SubFormat) error { - c.buffer = bytes.NewBufferString("") - c.preFormat() - - tmpl, err := c.parseFormat() - if err != nil { - return err - } - - subFormat := func(subContext subContext) error { - return c.contextFormat(tmpl, subContext) - } - if err := f(subFormat); err != nil { - return err - } - - c.postFormat(tmpl, sub) - return nil -} diff --git a/cli/command/formatter/history.go b/cli/command/formatter/history.go deleted file mode 100644 index 2b7de399a0..0000000000 --- a/cli/command/formatter/history.go +++ /dev/null @@ -1,113 +0,0 @@ -package formatter - -import ( - "strconv" - "strings" - "time" - - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/stringutils" - units "github.com/docker/go-units" -) - -const ( - defaultHistoryTableFormat = "table {{.ID}}\t{{.CreatedSince}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}" - nonHumanHistoryTableFormat = "table {{.ID}}\t{{.CreatedAt}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}" - - historyIDHeader = "IMAGE" - createdByHeader = "CREATED BY" - commentHeader = "COMMENT" -) - -// NewHistoryFormat returns a format for rendering an HistoryContext -func NewHistoryFormat(source string, quiet bool, human bool) Format { - switch source { - case TableFormatKey: - switch { - case quiet: - return defaultQuietFormat - case !human: - return nonHumanHistoryTableFormat - default: - return defaultHistoryTableFormat - } - } - - return Format(source) -} - -// HistoryWrite writes the context -func HistoryWrite(ctx Context, human bool, histories []image.HistoryResponseItem) error { - render := func(format func(subContext subContext) error) error { - for _, history := range histories { - historyCtx := &historyContext{trunc: ctx.Trunc, h: history, human: human} - if err := format(historyCtx); err != nil { - return err - } - } - return nil - } - historyCtx := &historyContext{} - historyCtx.header = map[string]string{ - "ID": historyIDHeader, - "CreatedSince": createdSinceHeader, - "CreatedAt": createdAtHeader, - "CreatedBy": createdByHeader, - "Size": sizeHeader, - "Comment": commentHeader, - } - return ctx.Write(historyCtx, render) -} - -type historyContext struct { - HeaderContext - trunc bool - human bool - h image.HistoryResponseItem -} - -func (c *historyContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *historyContext) ID() string { - if c.trunc { - return stringid.TruncateID(c.h.ID) - } - return c.h.ID -} - -func (c *historyContext) CreatedAt() string { - var created string - created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0))) - return created -} - -func (c *historyContext) CreatedSince() string { - var created string - created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0))) - return created + " ago" -} - -func (c *historyContext) CreatedBy() string { - createdBy := strings.Replace(c.h.CreatedBy, "\t", " ", -1) - if c.trunc { - createdBy = stringutils.Ellipsis(createdBy, 45) - } - return createdBy -} - -func (c *historyContext) Size() string { - size := "" - if c.human { - size = units.HumanSizeWithPrecision(float64(c.h.Size), 3) - } else { - size = strconv.FormatInt(c.h.Size, 10) - } - return size -} - -func (c *historyContext) Comment() string { - return c.h.Comment -} diff --git a/cli/command/formatter/history_test.go b/cli/command/formatter/history_test.go deleted file mode 100644 index ce80dc9b8b..0000000000 --- a/cli/command/formatter/history_test.go +++ /dev/null @@ -1,213 +0,0 @@ -package formatter - -import ( - "strconv" - "strings" - "testing" - "time" - - "bytes" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/stringutils" - "github.com/stretchr/testify/assert" -) - -type historyCase struct { - historyCtx historyContext - expValue string - call func() string -} - -func TestHistoryContext_ID(t *testing.T) { - id := stringid.GenerateRandomID() - - var ctx historyContext - cases := []historyCase{ - { - historyContext{ - h: image.HistoryResponseItem{ID: id}, - trunc: false, - }, id, ctx.ID, - }, - { - historyContext{ - h: image.HistoryResponseItem{ID: id}, - trunc: true, - }, stringid.TruncateID(id), ctx.ID, - }, - } - - for _, c := range cases { - ctx = c.historyCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestHistoryContext_CreatedSince(t *testing.T) { - unixTime := time.Now().AddDate(0, 0, -7).Unix() - expected := "7 days ago" - - var ctx historyContext - cases := []historyCase{ - { - historyContext{ - h: image.HistoryResponseItem{Created: unixTime}, - trunc: false, - human: true, - }, expected, ctx.CreatedSince, - }, - } - - for _, c := range cases { - ctx = c.historyCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestHistoryContext_CreatedBy(t *testing.T) { - withTabs := `/bin/sh -c apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 && echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates nginx=${NGINX_VERSION} nginx-module-xslt nginx-module-geoip nginx-module-image-filter nginx-module-perl nginx-module-njs gettext-base && rm -rf /var/lib/apt/lists/*` - expected := `/bin/sh -c apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 && echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates nginx=${NGINX_VERSION} nginx-module-xslt nginx-module-geoip nginx-module-image-filter nginx-module-perl nginx-module-njs gettext-base && rm -rf /var/lib/apt/lists/*` - - var ctx historyContext - cases := []historyCase{ - { - historyContext{ - h: image.HistoryResponseItem{CreatedBy: withTabs}, - trunc: false, - }, expected, ctx.CreatedBy, - }, - { - historyContext{ - h: image.HistoryResponseItem{CreatedBy: withTabs}, - trunc: true, - }, stringutils.Ellipsis(expected, 45), ctx.CreatedBy, - }, - } - - for _, c := range cases { - ctx = c.historyCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestHistoryContext_Size(t *testing.T) { - size := int64(182964289) - expected := "183MB" - - var ctx historyContext - cases := []historyCase{ - { - historyContext{ - h: image.HistoryResponseItem{Size: size}, - trunc: false, - human: true, - }, expected, ctx.Size, - }, { - historyContext{ - h: image.HistoryResponseItem{Size: size}, - trunc: false, - human: false, - }, strconv.Itoa(182964289), ctx.Size, - }, - } - - for _, c := range cases { - ctx = c.historyCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestHistoryContext_Comment(t *testing.T) { - comment := "Some comment" - - var ctx historyContext - cases := []historyCase{ - { - historyContext{ - h: image.HistoryResponseItem{Comment: comment}, - trunc: false, - }, comment, ctx.Comment, - }, - } - - for _, c := range cases { - ctx = c.historyCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestHistoryContext_Table(t *testing.T) { - out := bytes.NewBufferString("") - unixTime := time.Now().AddDate(0, 0, -1).Unix() - histories := []image.HistoryResponseItem{ - {ID: "imageID1", Created: unixTime, CreatedBy: "/bin/bash ls && npm i && npm run test && karma -c karma.conf.js start && npm start && more commands here && the list goes on", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, - {ID: "imageID2", Created: unixTime, CreatedBy: "/bin/bash echo", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, - {ID: "imageID3", Created: unixTime, CreatedBy: "/bin/bash ls", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, - {ID: "imageID4", Created: unixTime, CreatedBy: "/bin/bash grep", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, - } - expectedNoTrunc := `IMAGE CREATED CREATED BY SIZE COMMENT -imageID1 24 hours ago /bin/bash ls && npm i && npm run test && karma -c karma.conf.js start && npm start && more commands here && the list goes on 183MB Hi -imageID2 24 hours ago /bin/bash echo 183MB Hi -imageID3 24 hours ago /bin/bash ls 183MB Hi -imageID4 24 hours ago /bin/bash grep 183MB Hi -` - expectedTrunc := `IMAGE CREATED CREATED BY SIZE COMMENT -imageID1 24 hours ago /bin/bash ls && npm i && npm run test && k... 183MB Hi -imageID2 24 hours ago /bin/bash echo 183MB Hi -imageID3 24 hours ago /bin/bash ls 183MB Hi -imageID4 24 hours ago /bin/bash grep 183MB Hi -` - - contexts := []struct { - context Context - expected string - }{ - {Context{ - Format: NewHistoryFormat("table", false, true), - Trunc: true, - Output: out, - }, - expectedTrunc, - }, - {Context{ - Format: NewHistoryFormat("table", false, true), - Trunc: false, - Output: out, - }, - expectedNoTrunc, - }, - } - - for _, context := range contexts { - HistoryWrite(context.context, true, histories) - assert.Equal(t, context.expected, out.String()) - // Clean buffer - out.Reset() - } -} diff --git a/cli/command/formatter/image.go b/cli/command/formatter/image.go deleted file mode 100644 index 3aae34ea11..0000000000 --- a/cli/command/formatter/image.go +++ /dev/null @@ -1,272 +0,0 @@ -package formatter - -import ( - "fmt" - "time" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" - units "github.com/docker/go-units" -) - -const ( - defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}" - defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}" - - imageIDHeader = "IMAGE ID" - repositoryHeader = "REPOSITORY" - tagHeader = "TAG" - digestHeader = "DIGEST" -) - -// ImageContext contains image specific information required by the formatter, encapsulate a Context struct. -type ImageContext struct { - Context - Digest bool -} - -func isDangling(image types.ImageSummary) bool { - return len(image.RepoTags) == 1 && image.RepoTags[0] == ":" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "@" -} - -// NewImageFormat returns a format for rendering an ImageContext -func NewImageFormat(source string, quiet bool, digest bool) Format { - switch source { - case TableFormatKey: - switch { - case quiet: - return defaultQuietFormat - case digest: - return defaultImageTableFormatWithDigest - default: - return defaultImageTableFormat - } - case RawFormatKey: - switch { - case quiet: - return `image_id: {{.ID}}` - case digest: - return `repository: {{ .Repository }} -tag: {{.Tag}} -digest: {{.Digest}} -image_id: {{.ID}} -created_at: {{.CreatedAt}} -virtual_size: {{.Size}} -` - default: - return `repository: {{ .Repository }} -tag: {{.Tag}} -image_id: {{.ID}} -created_at: {{.CreatedAt}} -virtual_size: {{.Size}} -` - } - } - - format := Format(source) - if format.IsTable() && digest && !format.Contains("{{.Digest}}") { - format += "\t{{.Digest}}" - } - return format -} - -// ImageWrite writes the formatter images using the ImageContext -func ImageWrite(ctx ImageContext, images []types.ImageSummary) error { - render := func(format func(subContext subContext) error) error { - return imageFormat(ctx, images, format) - } - return ctx.Write(newImageContext(), render) -} - -func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext subContext) error) error { - for _, image := range images { - images := []*imageContext{} - if isDangling(image) { - images = append(images, &imageContext{ - trunc: ctx.Trunc, - i: image, - repo: "", - tag: "", - digest: "", - }) - } else { - repoTags := map[string][]string{} - repoDigests := map[string][]string{} - - for _, refString := range image.RepoTags { - ref, err := reference.ParseNormalizedNamed(refString) - if err != nil { - continue - } - if nt, ok := ref.(reference.NamedTagged); ok { - familiarRef := reference.FamiliarName(ref) - repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag()) - } - } - for _, refString := range image.RepoDigests { - ref, err := reference.ParseNormalizedNamed(refString) - if err != nil { - continue - } - if c, ok := ref.(reference.Canonical); ok { - familiarRef := reference.FamiliarName(ref) - repoDigests[familiarRef] = append(repoDigests[familiarRef], c.Digest().String()) - } - } - - for repo, tags := range repoTags { - digests := repoDigests[repo] - - // Do not display digests as their own row - delete(repoDigests, repo) - - if !ctx.Digest { - // Ignore digest references, just show tag once - digests = nil - } - - for _, tag := range tags { - if len(digests) == 0 { - images = append(images, &imageContext{ - trunc: ctx.Trunc, - i: image, - repo: repo, - tag: tag, - digest: "", - }) - continue - } - // Display the digests for each tag - for _, dgst := range digests { - images = append(images, &imageContext{ - trunc: ctx.Trunc, - i: image, - repo: repo, - tag: tag, - digest: dgst, - }) - } - - } - } - - // Show rows for remaining digest only references - for repo, digests := range repoDigests { - // If digests are displayed, show row per digest - if ctx.Digest { - for _, dgst := range digests { - images = append(images, &imageContext{ - trunc: ctx.Trunc, - i: image, - repo: repo, - tag: "", - digest: dgst, - }) - } - } else { - images = append(images, &imageContext{ - trunc: ctx.Trunc, - i: image, - repo: repo, - tag: "", - }) - } - } - } - for _, imageCtx := range images { - if err := format(imageCtx); err != nil { - return err - } - } - } - return nil -} - -type imageContext struct { - HeaderContext - trunc bool - i types.ImageSummary - repo string - tag string - digest string -} - -func newImageContext() *imageContext { - imageCtx := imageContext{} - imageCtx.header = map[string]string{ - "ID": imageIDHeader, - "Repository": repositoryHeader, - "Tag": tagHeader, - "Digest": digestHeader, - "CreatedSince": createdSinceHeader, - "CreatedAt": createdAtHeader, - "Size": sizeHeader, - "Containers": containersHeader, - "VirtualSize": sizeHeader, - "SharedSize": sharedSizeHeader, - "UniqueSize": uniqueSizeHeader, - } - return &imageCtx -} - -func (c *imageContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *imageContext) ID() string { - if c.trunc { - return stringid.TruncateID(c.i.ID) - } - return c.i.ID -} - -func (c *imageContext) Repository() string { - return c.repo -} - -func (c *imageContext) Tag() string { - return c.tag -} - -func (c *imageContext) Digest() string { - return c.digest -} - -func (c *imageContext) CreatedSince() string { - createdAt := time.Unix(int64(c.i.Created), 0) - return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago" -} - -func (c *imageContext) CreatedAt() string { - return time.Unix(int64(c.i.Created), 0).String() -} - -func (c *imageContext) Size() string { - return units.HumanSizeWithPrecision(float64(c.i.Size), 3) -} - -func (c *imageContext) Containers() string { - if c.i.Containers == -1 { - return "N/A" - } - return fmt.Sprintf("%d", c.i.Containers) -} - -func (c *imageContext) VirtualSize() string { - return units.HumanSize(float64(c.i.VirtualSize)) -} - -func (c *imageContext) SharedSize() string { - if c.i.SharedSize == -1 { - return "N/A" - } - return units.HumanSize(float64(c.i.SharedSize)) -} - -func (c *imageContext) UniqueSize() string { - if c.i.VirtualSize == -1 || c.i.SharedSize == -1 { - return "N/A" - } - return units.HumanSize(float64(c.i.VirtualSize - c.i.SharedSize)) -} diff --git a/cli/command/formatter/image_test.go b/cli/command/formatter/image_test.go deleted file mode 100644 index b3c4cc8094..0000000000 --- a/cli/command/formatter/image_test.go +++ /dev/null @@ -1,327 +0,0 @@ -package formatter - -import ( - "bytes" - "fmt" - "strings" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" - "github.com/stretchr/testify/assert" -) - -func TestImageContext(t *testing.T) { - imageID := stringid.GenerateRandomID() - unix := time.Now().Unix() - - var ctx imageContext - cases := []struct { - imageCtx imageContext - expValue string - call func() string - }{ - {imageContext{ - i: types.ImageSummary{ID: imageID}, - trunc: true, - }, stringid.TruncateID(imageID), ctx.ID}, - {imageContext{ - i: types.ImageSummary{ID: imageID}, - trunc: false, - }, imageID, ctx.ID}, - {imageContext{ - i: types.ImageSummary{Size: 10, VirtualSize: 10}, - trunc: true, - }, "10B", ctx.Size}, - {imageContext{ - i: types.ImageSummary{Created: unix}, - trunc: true, - }, time.Unix(unix, 0).String(), ctx.CreatedAt}, - // FIXME - // {imageContext{ - // i: types.ImageSummary{Created: unix}, - // trunc: true, - // }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince}, - {imageContext{ - i: types.ImageSummary{}, - repo: "busybox", - }, "busybox", ctx.Repository}, - {imageContext{ - i: types.ImageSummary{}, - tag: "latest", - }, "latest", ctx.Tag}, - {imageContext{ - i: types.ImageSummary{}, - digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", - }, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", ctx.Digest}, - } - - for _, c := range cases { - ctx = c.imageCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestImageContextWrite(t *testing.T) { - unixTime := time.Now().AddDate(0, 0, -1).Unix() - expectedTime := time.Unix(unixTime, 0).String() - - cases := []struct { - context ImageContext - expected string - }{ - // Errors - { - ImageContext{ - Context: Context{ - Format: "{{InvalidFunction}}", - }, - }, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - ImageContext{ - Context: Context{ - Format: "{{nil}}", - }, - }, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table Format - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table", false, false), - }, - }, - `REPOSITORY TAG IMAGE ID CREATED SIZE -image tag1 imageID1 24 hours ago 0B -image tag2 imageID2 24 hours ago 0B - imageID3 24 hours ago 0B -`, - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table {{.Repository}}", false, false), - }, - }, - "REPOSITORY\nimage\nimage\n\n", - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table {{.Repository}}", false, true), - }, - Digest: true, - }, - `REPOSITORY DIGEST -image sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf -image - -`, - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table {{.Repository}}", true, false), - }, - }, - "REPOSITORY\nimage\nimage\n\n", - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table", true, false), - }, - }, - "imageID1\nimageID2\nimageID3\n", - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table", false, true), - }, - Digest: true, - }, - `REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE -image tag1 sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf imageID1 24 hours ago 0B -image tag2 imageID2 24 hours ago 0B - imageID3 24 hours ago 0B -`, - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table", true, true), - }, - Digest: true, - }, - "imageID1\nimageID2\nimageID3\n", - }, - // Raw Format - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("raw", false, false), - }, - }, - fmt.Sprintf(`repository: image -tag: tag1 -image_id: imageID1 -created_at: %s -virtual_size: 0B - -repository: image -tag: tag2 -image_id: imageID2 -created_at: %s -virtual_size: 0B - -repository: -tag: -image_id: imageID3 -created_at: %s -virtual_size: 0B - -`, expectedTime, expectedTime, expectedTime), - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("raw", false, true), - }, - Digest: true, - }, - fmt.Sprintf(`repository: image -tag: tag1 -digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf -image_id: imageID1 -created_at: %s -virtual_size: 0B - -repository: image -tag: tag2 -digest: -image_id: imageID2 -created_at: %s -virtual_size: 0B - -repository: -tag: -digest: -image_id: imageID3 -created_at: %s -virtual_size: 0B - -`, expectedTime, expectedTime, expectedTime), - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("raw", true, false), - }, - }, - `image_id: imageID1 -image_id: imageID2 -image_id: imageID3 -`, - }, - // Custom Format - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("{{.Repository}}", false, false), - }, - }, - "image\nimage\n\n", - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("{{.Repository}}", false, true), - }, - Digest: true, - }, - "image\nimage\n\n", - }, - } - - for _, testcase := range cases { - images := []types.ImageSummary{ - {ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime}, - {ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime}, - {ID: "imageID3", RepoTags: []string{":"}, RepoDigests: []string{"@"}, Created: unixTime}, - } - out := bytes.NewBufferString("") - testcase.context.Output = out - err := ImageWrite(testcase.context, images) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} - -func TestImageContextWriteWithNoImage(t *testing.T) { - out := bytes.NewBufferString("") - images := []types.ImageSummary{} - - contexts := []struct { - context ImageContext - expected string - }{ - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("{{.Repository}}", false, false), - Output: out, - }, - }, - "", - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table {{.Repository}}", false, false), - Output: out, - }, - }, - "REPOSITORY\n", - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("{{.Repository}}", false, true), - Output: out, - }, - }, - "", - }, - { - ImageContext{ - Context: Context{ - Format: NewImageFormat("table {{.Repository}}", false, true), - Output: out, - }, - }, - "REPOSITORY DIGEST\n", - }, - } - - for _, context := range contexts { - ImageWrite(context.context, images) - assert.Equal(t, context.expected, out.String()) - // Clean buffer - out.Reset() - } -} diff --git a/cli/command/formatter/network.go b/cli/command/formatter/network.go deleted file mode 100644 index 4aeebd1750..0000000000 --- a/cli/command/formatter/network.go +++ /dev/null @@ -1,129 +0,0 @@ -package formatter - -import ( - "fmt" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" -) - -const ( - defaultNetworkTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.Scope}}" - - networkIDHeader = "NETWORK ID" - ipv6Header = "IPV6" - internalHeader = "INTERNAL" -) - -// NewNetworkFormat returns a Format for rendering using a network Context -func NewNetworkFormat(source string, quiet bool) Format { - switch source { - case TableFormatKey: - if quiet { - return defaultQuietFormat - } - return defaultNetworkTableFormat - case RawFormatKey: - if quiet { - return `network_id: {{.ID}}` - } - return `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n` - } - return Format(source) -} - -// NetworkWrite writes the context -func NetworkWrite(ctx Context, networks []types.NetworkResource) error { - render := func(format func(subContext subContext) error) error { - for _, network := range networks { - networkCtx := &networkContext{trunc: ctx.Trunc, n: network} - if err := format(networkCtx); err != nil { - return err - } - } - return nil - } - networkCtx := networkContext{} - networkCtx.header = networkHeaderContext{ - "ID": networkIDHeader, - "Name": nameHeader, - "Driver": driverHeader, - "Scope": scopeHeader, - "IPv6": ipv6Header, - "Internal": internalHeader, - "Labels": labelsHeader, - "CreatedAt": createdAtHeader, - } - return ctx.Write(&networkCtx, render) -} - -type networkHeaderContext map[string]string - -func (c networkHeaderContext) Label(name string) string { - n := strings.Split(name, ".") - r := strings.NewReplacer("-", " ", "_", " ") - h := r.Replace(n[len(n)-1]) - - return h -} - -type networkContext struct { - HeaderContext - trunc bool - n types.NetworkResource -} - -func (c *networkContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *networkContext) ID() string { - if c.trunc { - return stringid.TruncateID(c.n.ID) - } - return c.n.ID -} - -func (c *networkContext) Name() string { - return c.n.Name -} - -func (c *networkContext) Driver() string { - return c.n.Driver -} - -func (c *networkContext) Scope() string { - return c.n.Scope -} - -func (c *networkContext) IPv6() string { - return fmt.Sprintf("%v", c.n.EnableIPv6) -} - -func (c *networkContext) Internal() string { - return fmt.Sprintf("%v", c.n.Internal) -} - -func (c *networkContext) Labels() string { - if c.n.Labels == nil { - return "" - } - - var joinLabels []string - for k, v := range c.n.Labels { - joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) - } - return strings.Join(joinLabels, ",") -} - -func (c *networkContext) Label(name string) string { - if c.n.Labels == nil { - return "" - } - return c.n.Labels[name] -} - -func (c *networkContext) CreatedAt() string { - return c.n.Created.String() -} diff --git a/cli/command/formatter/network_test.go b/cli/command/formatter/network_test.go deleted file mode 100644 index b8cab078e7..0000000000 --- a/cli/command/formatter/network_test.go +++ /dev/null @@ -1,213 +0,0 @@ -package formatter - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" - "github.com/stretchr/testify/assert" -) - -func TestNetworkContext(t *testing.T) { - networkID := stringid.GenerateRandomID() - - var ctx networkContext - cases := []struct { - networkCtx networkContext - expValue string - call func() string - }{ - {networkContext{ - n: types.NetworkResource{ID: networkID}, - trunc: false, - }, networkID, ctx.ID}, - {networkContext{ - n: types.NetworkResource{ID: networkID}, - trunc: true, - }, stringid.TruncateID(networkID), ctx.ID}, - {networkContext{ - n: types.NetworkResource{Name: "network_name"}, - }, "network_name", ctx.Name}, - {networkContext{ - n: types.NetworkResource{Driver: "driver_name"}, - }, "driver_name", ctx.Driver}, - {networkContext{ - n: types.NetworkResource{EnableIPv6: true}, - }, "true", ctx.IPv6}, - {networkContext{ - n: types.NetworkResource{EnableIPv6: false}, - }, "false", ctx.IPv6}, - {networkContext{ - n: types.NetworkResource{Internal: true}, - }, "true", ctx.Internal}, - {networkContext{ - n: types.NetworkResource{Internal: false}, - }, "false", ctx.Internal}, - {networkContext{ - n: types.NetworkResource{}, - }, "", ctx.Labels}, - {networkContext{ - n: types.NetworkResource{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, - }, "label1=value1,label2=value2", ctx.Labels}, - } - - for _, c := range cases { - ctx = c.networkCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestNetworkContextWrite(t *testing.T) { - cases := []struct { - context Context - expected string - }{ - - // Errors - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table format - { - Context{Format: NewNetworkFormat("table", false)}, - `NETWORK ID NAME DRIVER SCOPE -networkID1 foobar_baz foo local -networkID2 foobar_bar bar local -`, - }, - { - Context{Format: NewNetworkFormat("table", true)}, - `networkID1 -networkID2 -`, - }, - { - Context{Format: NewNetworkFormat("table {{.Name}}", false)}, - `NAME -foobar_baz -foobar_bar -`, - }, - { - Context{Format: NewNetworkFormat("table {{.Name}}", true)}, - `NAME -foobar_baz -foobar_bar -`, - }, - // Raw Format - { - Context{Format: NewNetworkFormat("raw", false)}, - `network_id: networkID1 -name: foobar_baz -driver: foo -scope: local - -network_id: networkID2 -name: foobar_bar -driver: bar -scope: local - -`, - }, - { - Context{Format: NewNetworkFormat("raw", true)}, - `network_id: networkID1 -network_id: networkID2 -`, - }, - // Custom Format - { - Context{Format: NewNetworkFormat("{{.Name}}", false)}, - `foobar_baz -foobar_bar -`, - }, - // Custom Format with CreatedAt - { - Context{Format: NewNetworkFormat("{{.Name}} {{.CreatedAt}}", false)}, - `foobar_baz 2016-01-01 00:00:00 +0000 UTC -foobar_bar 2017-01-01 00:00:00 +0000 UTC -`, - }, - } - - timestamp1, _ := time.Parse("2006-01-02", "2016-01-01") - timestamp2, _ := time.Parse("2006-01-02", "2017-01-01") - - for _, testcase := range cases { - networks := []types.NetworkResource{ - {ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local", Created: timestamp1}, - {ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local", Created: timestamp2}, - } - out := bytes.NewBufferString("") - testcase.context.Output = out - err := NetworkWrite(testcase.context, networks) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} - -func TestNetworkContextWriteJSON(t *testing.T) { - networks := []types.NetworkResource{ - {ID: "networkID1", Name: "foobar_baz"}, - {ID: "networkID2", Name: "foobar_bar"}, - } - expectedJSONs := []map[string]interface{}{ - {"Driver": "", "ID": "networkID1", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_baz", "Scope": "", "CreatedAt": "0001-01-01 00:00:00 +0000 UTC"}, - {"Driver": "", "ID": "networkID2", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_bar", "Scope": "", "CreatedAt": "0001-01-01 00:00:00 +0000 UTC"}, - } - - out := bytes.NewBufferString("") - err := NetworkWrite(Context{Format: "{{json .}}", Output: out}, networks) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var m map[string]interface{} - if err := json.Unmarshal([]byte(line), &m); err != nil { - t.Fatal(err) - } - assert.Equal(t, expectedJSONs[i], m) - } -} - -func TestNetworkContextWriteJSONField(t *testing.T) { - networks := []types.NetworkResource{ - {ID: "networkID1", Name: "foobar_baz"}, - {ID: "networkID2", Name: "foobar_bar"}, - } - out := bytes.NewBufferString("") - err := NetworkWrite(Context{Format: "{{json .ID}}", Output: out}, networks) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var s string - if err := json.Unmarshal([]byte(line), &s); err != nil { - t.Fatal(err) - } - assert.Equal(t, networks[i].ID, s) - } -} diff --git a/cli/command/formatter/node.go b/cli/command/formatter/node.go deleted file mode 100644 index 4d7f293f5c..0000000000 --- a/cli/command/formatter/node.go +++ /dev/null @@ -1,292 +0,0 @@ -package formatter - -import ( - "fmt" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" - units "github.com/docker/go-units" -) - -const ( - defaultNodeTableFormat = "table {{.ID}} {{if .Self}}*{{else}} {{ end }}\t{{.Hostname}}\t{{.Status}}\t{{.Availability}}\t{{.ManagerStatus}}" - nodeInspectPrettyTemplate Format = `ID: {{.ID}} -{{- if .Name }} -Name: {{.Name}} -{{- end }} -{{- if .Labels }} -Labels: -{{- range $k, $v := .Labels }} - - {{ $k }}{{if $v }}={{ $v }}{{ end }} -{{- end }}{{ end }} -Hostname: {{.Hostname}} -Joined at: {{.CreatedAt}} -Status: - State: {{.StatusState}} - {{- if .HasStatusMessage}} - Message: {{.StatusMessage}} - {{- end}} - Availability: {{.SpecAvailability}} - {{- if .Status.Addr}} - Address: {{.StatusAddr}} - {{- end}} -{{- if .HasManagerStatus}} -Manager Status: - Address: {{.ManagerStatusAddr}} - Raft Status: {{.ManagerStatusReachability}} - {{- if .IsManagerStatusLeader}} - Leader: Yes - {{- else}} - Leader: No - {{- end}} -{{- end}} -Platform: - Operating System: {{.PlatformOS}} - Architecture: {{.PlatformArchitecture}} -Resources: - CPUs: {{.ResourceNanoCPUs}} - Memory: {{.ResourceMemory}} -{{- if .HasEnginePlugins}} -Plugins: -{{- range $k, $v := .EnginePlugins }} - {{ $k }}:{{if $v }} {{ $v }}{{ end }} -{{- end }} -{{- end }} -Engine Version: {{.EngineVersion}} -{{- if .EngineLabels}} -Engine Labels: -{{- range $k, $v := .EngineLabels }} - - {{ $k }}{{if $v }}={{ $v }}{{ end }} -{{- end }}{{- end }} -` - nodeIDHeader = "ID" - selfHeader = "" - hostnameHeader = "HOSTNAME" - availabilityHeader = "AVAILABILITY" - managerStatusHeader = "MANAGER STATUS" -) - -// NewNodeFormat returns a Format for rendering using a node Context -func NewNodeFormat(source string, quiet bool) Format { - switch source { - case PrettyFormatKey: - return nodeInspectPrettyTemplate - case TableFormatKey: - if quiet { - return defaultQuietFormat - } - return defaultNodeTableFormat - case RawFormatKey: - if quiet { - return `node_id: {{.ID}}` - } - return `node_id: {{.ID}}\nhostname: {{.Hostname}}\nstatus: {{.Status}}\navailability: {{.Availability}}\nmanager_status: {{.ManagerStatus}}\n` - } - return Format(source) -} - -// NodeWrite writes the context -func NodeWrite(ctx Context, nodes []swarm.Node, info types.Info) error { - render := func(format func(subContext subContext) error) error { - for _, node := range nodes { - nodeCtx := &nodeContext{n: node, info: info} - if err := format(nodeCtx); err != nil { - return err - } - } - return nil - } - nodeCtx := nodeContext{} - nodeCtx.header = nodeHeaderContext{ - "ID": nodeIDHeader, - "Self": selfHeader, - "Hostname": hostnameHeader, - "Status": statusHeader, - "Availability": availabilityHeader, - "ManagerStatus": managerStatusHeader, - } - return ctx.Write(&nodeCtx, render) -} - -type nodeHeaderContext map[string]string - -type nodeContext struct { - HeaderContext - n swarm.Node - info types.Info -} - -func (c *nodeContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *nodeContext) ID() string { - return c.n.ID -} - -func (c *nodeContext) Self() bool { - return c.n.ID == c.info.Swarm.NodeID -} - -func (c *nodeContext) Hostname() string { - return c.n.Description.Hostname -} - -func (c *nodeContext) Status() string { - return command.PrettyPrint(string(c.n.Status.State)) -} - -func (c *nodeContext) Availability() string { - return command.PrettyPrint(string(c.n.Spec.Availability)) -} - -func (c *nodeContext) ManagerStatus() string { - reachability := "" - if c.n.ManagerStatus != nil { - if c.n.ManagerStatus.Leader { - reachability = "Leader" - } else { - reachability = string(c.n.ManagerStatus.Reachability) - } - } - return command.PrettyPrint(reachability) -} - -// NodeInspectWrite renders the context for a list of services -func NodeInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) error { - if ctx.Format != nodeInspectPrettyTemplate { - return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) - } - render := func(format func(subContext subContext) error) error { - for _, ref := range refs { - nodeI, _, err := getRef(ref) - if err != nil { - return err - } - node, ok := nodeI.(swarm.Node) - if !ok { - return fmt.Errorf("got wrong object to inspect :%v", ok) - } - if err := format(&nodeInspectContext{Node: node}); err != nil { - return err - } - } - return nil - } - return ctx.Write(&nodeInspectContext{}, render) -} - -type nodeInspectContext struct { - swarm.Node - subContext -} - -func (ctx *nodeInspectContext) ID() string { - return ctx.Node.ID -} - -func (ctx *nodeInspectContext) Name() string { - return ctx.Node.Spec.Name -} - -func (ctx *nodeInspectContext) Labels() map[string]string { - return ctx.Node.Spec.Labels -} - -func (ctx *nodeInspectContext) Hostname() string { - return ctx.Node.Description.Hostname -} - -func (ctx *nodeInspectContext) CreatedAt() string { - return command.PrettyPrint(ctx.Node.CreatedAt) -} - -func (ctx *nodeInspectContext) StatusState() string { - return command.PrettyPrint(ctx.Node.Status.State) -} - -func (ctx *nodeInspectContext) HasStatusMessage() bool { - return ctx.Node.Status.Message != "" -} - -func (ctx *nodeInspectContext) StatusMessage() string { - return command.PrettyPrint(ctx.Node.Status.Message) -} - -func (ctx *nodeInspectContext) SpecAvailability() string { - return command.PrettyPrint(ctx.Node.Spec.Availability) -} - -func (ctx *nodeInspectContext) HasStatusAddr() bool { - return ctx.Node.Status.Addr != "" -} - -func (ctx *nodeInspectContext) StatusAddr() string { - return ctx.Node.Status.Addr -} - -func (ctx *nodeInspectContext) HasManagerStatus() bool { - return ctx.Node.ManagerStatus != nil -} - -func (ctx *nodeInspectContext) ManagerStatusAddr() string { - return ctx.Node.ManagerStatus.Addr -} - -func (ctx *nodeInspectContext) ManagerStatusReachability() string { - return command.PrettyPrint(ctx.Node.ManagerStatus.Reachability) -} - -func (ctx *nodeInspectContext) IsManagerStatusLeader() bool { - return ctx.Node.ManagerStatus.Leader -} - -func (ctx *nodeInspectContext) PlatformOS() string { - return ctx.Node.Description.Platform.OS -} - -func (ctx *nodeInspectContext) PlatformArchitecture() string { - return ctx.Node.Description.Platform.Architecture -} - -func (ctx *nodeInspectContext) ResourceNanoCPUs() int { - if ctx.Node.Description.Resources.NanoCPUs == 0 { - return int(0) - } - return int(ctx.Node.Description.Resources.NanoCPUs) / 1e9 -} - -func (ctx *nodeInspectContext) ResourceMemory() string { - if ctx.Node.Description.Resources.MemoryBytes == 0 { - return "" - } - return units.BytesSize(float64(ctx.Node.Description.Resources.MemoryBytes)) -} - -func (ctx *nodeInspectContext) HasEnginePlugins() bool { - return len(ctx.Node.Description.Engine.Plugins) > 0 -} - -func (ctx *nodeInspectContext) EnginePlugins() map[string]string { - pluginMap := map[string][]string{} - for _, p := range ctx.Node.Description.Engine.Plugins { - pluginMap[p.Type] = append(pluginMap[p.Type], p.Name) - } - - pluginNamesByType := map[string]string{} - for k, v := range pluginMap { - pluginNamesByType[k] = strings.Join(v, ", ") - } - return pluginNamesByType -} - -func (ctx *nodeInspectContext) EngineLabels() map[string]string { - return ctx.Node.Description.Engine.Labels -} - -func (ctx *nodeInspectContext) EngineVersion() string { - return ctx.Node.Description.Engine.EngineVersion -} diff --git a/cli/command/formatter/node_test.go b/cli/command/formatter/node_test.go deleted file mode 100644 index ea2f4ce4d5..0000000000 --- a/cli/command/formatter/node_test.go +++ /dev/null @@ -1,188 +0,0 @@ -package formatter - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/pkg/stringid" - "github.com/stretchr/testify/assert" -) - -func TestNodeContext(t *testing.T) { - nodeID := stringid.GenerateRandomID() - - var ctx nodeContext - cases := []struct { - nodeCtx nodeContext - expValue string - call func() string - }{ - {nodeContext{ - n: swarm.Node{ID: nodeID}, - }, nodeID, ctx.ID}, - {nodeContext{ - n: swarm.Node{Description: swarm.NodeDescription{Hostname: "node_hostname"}}, - }, "node_hostname", ctx.Hostname}, - {nodeContext{ - n: swarm.Node{Status: swarm.NodeStatus{State: swarm.NodeState("foo")}}, - }, "Foo", ctx.Status}, - {nodeContext{ - n: swarm.Node{Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}}, - }, "Drain", ctx.Availability}, - {nodeContext{ - n: swarm.Node{ManagerStatus: &swarm.ManagerStatus{Leader: true}}, - }, "Leader", ctx.ManagerStatus}, - } - - for _, c := range cases { - ctx = c.nodeCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestNodeContextWrite(t *testing.T) { - cases := []struct { - context Context - expected string - }{ - - // Errors - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table format - { - Context{Format: NewNodeFormat("table", false)}, - `ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS -nodeID1 foobar_baz Foo Drain Leader -nodeID2 foobar_bar Bar Active Reachable -`, - }, - { - Context{Format: NewNodeFormat("table", true)}, - `nodeID1 -nodeID2 -`, - }, - { - Context{Format: NewNodeFormat("table {{.Hostname}}", false)}, - `HOSTNAME -foobar_baz -foobar_bar -`, - }, - { - Context{Format: NewNodeFormat("table {{.Hostname}}", true)}, - `HOSTNAME -foobar_baz -foobar_bar -`, - }, - // Raw Format - { - Context{Format: NewNodeFormat("raw", false)}, - `node_id: nodeID1 -hostname: foobar_baz -status: Foo -availability: Drain -manager_status: Leader - -node_id: nodeID2 -hostname: foobar_bar -status: Bar -availability: Active -manager_status: Reachable - -`, - }, - { - Context{Format: NewNodeFormat("raw", true)}, - `node_id: nodeID1 -node_id: nodeID2 -`, - }, - // Custom Format - { - Context{Format: NewNodeFormat("{{.Hostname}}", false)}, - `foobar_baz -foobar_bar -`, - }, - } - - for _, testcase := range cases { - nodes := []swarm.Node{ - {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}, Status: swarm.NodeStatus{State: swarm.NodeState("foo")}, Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}, ManagerStatus: &swarm.ManagerStatus{Leader: true}}, - {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}, Status: swarm.NodeStatus{State: swarm.NodeState("bar")}, Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("active")}, ManagerStatus: &swarm.ManagerStatus{Leader: false, Reachability: swarm.Reachability("Reachable")}}, - } - out := bytes.NewBufferString("") - testcase.context.Output = out - err := NodeWrite(testcase.context, nodes, types.Info{}) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} - -func TestNodeContextWriteJSON(t *testing.T) { - nodes := []swarm.Node{ - {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}}, - {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}}, - } - expectedJSONs := []map[string]interface{}{ - {"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": "", "Self": false}, - {"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false}, - } - - out := bytes.NewBufferString("") - err := NodeWrite(Context{Format: "{{json .}}", Output: out}, nodes, types.Info{}) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var m map[string]interface{} - if err := json.Unmarshal([]byte(line), &m); err != nil { - t.Fatal(err) - } - assert.Equal(t, expectedJSONs[i], m) - } -} - -func TestNodeContextWriteJSONField(t *testing.T) { - nodes := []swarm.Node{ - {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}}, - {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}}, - } - out := bytes.NewBufferString("") - err := NodeWrite(Context{Format: "{{json .ID}}", Output: out}, nodes, types.Info{}) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var s string - if err := json.Unmarshal([]byte(line), &s); err != nil { - t.Fatal(err) - } - assert.Equal(t, nodes[i].ID, s) - } -} diff --git a/cli/command/formatter/plugin.go b/cli/command/formatter/plugin.go deleted file mode 100644 index 2b71281a58..0000000000 --- a/cli/command/formatter/plugin.go +++ /dev/null @@ -1,95 +0,0 @@ -package formatter - -import ( - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/stringutils" -) - -const ( - defaultPluginTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Description}}\t{{.Enabled}}" - - pluginIDHeader = "ID" - descriptionHeader = "DESCRIPTION" - enabledHeader = "ENABLED" -) - -// NewPluginFormat returns a Format for rendering using a plugin Context -func NewPluginFormat(source string, quiet bool) Format { - switch source { - case TableFormatKey: - if quiet { - return defaultQuietFormat - } - return defaultPluginTableFormat - case RawFormatKey: - if quiet { - return `plugin_id: {{.ID}}` - } - return `plugin_id: {{.ID}}\nname: {{.Name}}\ndescription: {{.Description}}\nenabled: {{.Enabled}}\n` - } - return Format(source) -} - -// PluginWrite writes the context -func PluginWrite(ctx Context, plugins []*types.Plugin) error { - render := func(format func(subContext subContext) error) error { - for _, plugin := range plugins { - pluginCtx := &pluginContext{trunc: ctx.Trunc, p: *plugin} - if err := format(pluginCtx); err != nil { - return err - } - } - return nil - } - pluginCtx := pluginContext{} - pluginCtx.header = map[string]string{ - "ID": pluginIDHeader, - "Name": nameHeader, - "Description": descriptionHeader, - "Enabled": enabledHeader, - "PluginReference": imageHeader, - } - return ctx.Write(&pluginCtx, render) -} - -type pluginContext struct { - HeaderContext - trunc bool - p types.Plugin -} - -func (c *pluginContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *pluginContext) ID() string { - if c.trunc { - return stringid.TruncateID(c.p.ID) - } - return c.p.ID -} - -func (c *pluginContext) Name() string { - return c.p.Name -} - -func (c *pluginContext) Description() string { - desc := strings.Replace(c.p.Config.Description, "\n", "", -1) - desc = strings.Replace(desc, "\r", "", -1) - if c.trunc { - desc = stringutils.Ellipsis(desc, 45) - } - - return desc -} - -func (c *pluginContext) Enabled() bool { - return c.p.Enabled -} - -func (c *pluginContext) PluginReference() string { - return c.p.PluginReference -} diff --git a/cli/command/formatter/plugin_test.go b/cli/command/formatter/plugin_test.go deleted file mode 100644 index 607262dcc9..0000000000 --- a/cli/command/formatter/plugin_test.go +++ /dev/null @@ -1,182 +0,0 @@ -package formatter - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" - "github.com/stretchr/testify/assert" -) - -func TestPluginContext(t *testing.T) { - pluginID := stringid.GenerateRandomID() - - var ctx pluginContext - cases := []struct { - pluginCtx pluginContext - expValue string - call func() string - }{ - {pluginContext{ - p: types.Plugin{ID: pluginID}, - trunc: false, - }, pluginID, ctx.ID}, - {pluginContext{ - p: types.Plugin{ID: pluginID}, - trunc: true, - }, stringid.TruncateID(pluginID), ctx.ID}, - {pluginContext{ - p: types.Plugin{Name: "plugin_name"}, - }, "plugin_name", ctx.Name}, - {pluginContext{ - p: types.Plugin{Config: types.PluginConfig{Description: "plugin_description"}}, - }, "plugin_description", ctx.Description}, - } - - for _, c := range cases { - ctx = c.pluginCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestPluginContextWrite(t *testing.T) { - cases := []struct { - context Context - expected string - }{ - - // Errors - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table format - { - Context{Format: NewPluginFormat("table", false)}, - `ID NAME DESCRIPTION ENABLED -pluginID1 foobar_baz description 1 true -pluginID2 foobar_bar description 2 false -`, - }, - { - Context{Format: NewPluginFormat("table", true)}, - `pluginID1 -pluginID2 -`, - }, - { - Context{Format: NewPluginFormat("table {{.Name}}", false)}, - `NAME -foobar_baz -foobar_bar -`, - }, - { - Context{Format: NewPluginFormat("table {{.Name}}", true)}, - `NAME -foobar_baz -foobar_bar -`, - }, - // Raw Format - { - Context{Format: NewPluginFormat("raw", false)}, - `plugin_id: pluginID1 -name: foobar_baz -description: description 1 -enabled: true - -plugin_id: pluginID2 -name: foobar_bar -description: description 2 -enabled: false - -`, - }, - { - Context{Format: NewPluginFormat("raw", true)}, - `plugin_id: pluginID1 -plugin_id: pluginID2 -`, - }, - // Custom Format - { - Context{Format: NewPluginFormat("{{.Name}}", false)}, - `foobar_baz -foobar_bar -`, - }, - } - - for _, testcase := range cases { - plugins := []*types.Plugin{ - {ID: "pluginID1", Name: "foobar_baz", Config: types.PluginConfig{Description: "description 1"}, Enabled: true}, - {ID: "pluginID2", Name: "foobar_bar", Config: types.PluginConfig{Description: "description 2"}, Enabled: false}, - } - out := bytes.NewBufferString("") - testcase.context.Output = out - err := PluginWrite(testcase.context, plugins) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} - -func TestPluginContextWriteJSON(t *testing.T) { - plugins := []*types.Plugin{ - {ID: "pluginID1", Name: "foobar_baz"}, - {ID: "pluginID2", Name: "foobar_bar"}, - } - expectedJSONs := []map[string]interface{}{ - {"Description": "", "Enabled": false, "ID": "pluginID1", "Name": "foobar_baz", "PluginReference": ""}, - {"Description": "", "Enabled": false, "ID": "pluginID2", "Name": "foobar_bar", "PluginReference": ""}, - } - - out := bytes.NewBufferString("") - err := PluginWrite(Context{Format: "{{json .}}", Output: out}, plugins) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - var m map[string]interface{} - if err := json.Unmarshal([]byte(line), &m); err != nil { - t.Fatal(err) - } - assert.Equal(t, expectedJSONs[i], m) - } -} - -func TestPluginContextWriteJSONField(t *testing.T) { - plugins := []*types.Plugin{ - {ID: "pluginID1", Name: "foobar_baz"}, - {ID: "pluginID2", Name: "foobar_bar"}, - } - out := bytes.NewBufferString("") - err := PluginWrite(Context{Format: "{{json .ID}}", Output: out}, plugins) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - var s string - if err := json.Unmarshal([]byte(line), &s); err != nil { - t.Fatal(err) - } - assert.Equal(t, plugins[i].ID, s) - } -} diff --git a/cli/command/formatter/reflect.go b/cli/command/formatter/reflect.go deleted file mode 100644 index fd59404d05..0000000000 --- a/cli/command/formatter/reflect.go +++ /dev/null @@ -1,66 +0,0 @@ -package formatter - -import ( - "encoding/json" - "reflect" - "unicode" - - "github.com/pkg/errors" -) - -func marshalJSON(x interface{}) ([]byte, error) { - m, err := marshalMap(x) - if err != nil { - return nil, err - } - return json.Marshal(m) -} - -// marshalMap marshals x to map[string]interface{} -func marshalMap(x interface{}) (map[string]interface{}, error) { - val := reflect.ValueOf(x) - if val.Kind() != reflect.Ptr { - return nil, errors.Errorf("expected a pointer to a struct, got %v", val.Kind()) - } - if val.IsNil() { - return nil, errors.Errorf("expected a pointer to a struct, got nil pointer") - } - valElem := val.Elem() - if valElem.Kind() != reflect.Struct { - return nil, errors.Errorf("expected a pointer to a struct, got a pointer to %v", valElem.Kind()) - } - typ := val.Type() - m := make(map[string]interface{}) - for i := 0; i < val.NumMethod(); i++ { - k, v, err := marshalForMethod(typ.Method(i), val.Method(i)) - if err != nil { - return nil, err - } - if k != "" { - m[k] = v - } - } - return m, nil -} - -var unmarshallableNames = map[string]struct{}{"FullHeader": {}} - -// marshalForMethod returns the map key and the map value for marshalling the method. -// It returns ("", nil, nil) for valid but non-marshallable parameter. (e.g. "unexportedFunc()") -func marshalForMethod(typ reflect.Method, val reflect.Value) (string, interface{}, error) { - if val.Kind() != reflect.Func { - return "", nil, errors.Errorf("expected func, got %v", val.Kind()) - } - name, numIn, numOut := typ.Name, val.Type().NumIn(), val.Type().NumOut() - _, blackListed := unmarshallableNames[name] - // FIXME: In text/template, (numOut == 2) is marshallable, - // if the type of the second param is error. - marshallable := unicode.IsUpper(rune(name[0])) && !blackListed && - numIn == 0 && numOut == 1 - if !marshallable { - return "", nil, nil - } - result := val.Call(make([]reflect.Value, numIn)) - intf := result[0].Interface() - return name, intf, nil -} diff --git a/cli/command/formatter/reflect_test.go b/cli/command/formatter/reflect_test.go deleted file mode 100644 index e547b18411..0000000000 --- a/cli/command/formatter/reflect_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package formatter - -import ( - "reflect" - "testing" -) - -type dummy struct { -} - -func (d *dummy) Func1() string { - return "Func1" -} - -func (d *dummy) func2() string { - return "func2(should not be marshalled)" -} - -func (d *dummy) Func3() (string, int) { - return "Func3(should not be marshalled)", -42 -} - -func (d *dummy) Func4() int { - return 4 -} - -type dummyType string - -func (d *dummy) Func5() dummyType { - return dummyType("Func5") -} - -func (d *dummy) FullHeader() string { - return "FullHeader(should not be marshalled)" -} - -var dummyExpected = map[string]interface{}{ - "Func1": "Func1", - "Func4": 4, - "Func5": dummyType("Func5"), -} - -func TestMarshalMap(t *testing.T) { - d := dummy{} - m, err := marshalMap(&d) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(dummyExpected, m) { - t.Fatalf("expected %+v, got %+v", - dummyExpected, m) - } -} - -func TestMarshalMapBad(t *testing.T) { - if _, err := marshalMap(nil); err == nil { - t.Fatal("expected an error (argument is nil)") - } - if _, err := marshalMap(dummy{}); err == nil { - t.Fatal("expected an error (argument is non-pointer)") - } - x := 42 - if _, err := marshalMap(&x); err == nil { - t.Fatal("expected an error (argument is a pointer to non-struct)") - } -} diff --git a/cli/command/formatter/secret.go b/cli/command/formatter/secret.go deleted file mode 100644 index 7ec6f9a62e..0000000000 --- a/cli/command/formatter/secret.go +++ /dev/null @@ -1,101 +0,0 @@ -package formatter - -import ( - "fmt" - "strings" - "time" - - "github.com/docker/docker/api/types/swarm" - units "github.com/docker/go-units" -) - -const ( - defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}" - secretIDHeader = "ID" - secretNameHeader = "NAME" - secretCreatedHeader = "CREATED" - secretUpdatedHeader = "UPDATED" -) - -// NewSecretFormat returns a Format for rendering using a network Context -func NewSecretFormat(source string, quiet bool) Format { - switch source { - case TableFormatKey: - if quiet { - return defaultQuietFormat - } - return defaultSecretTableFormat - } - return Format(source) -} - -// SecretWrite writes the context -func SecretWrite(ctx Context, secrets []swarm.Secret) error { - render := func(format func(subContext subContext) error) error { - for _, secret := range secrets { - secretCtx := &secretContext{s: secret} - if err := format(secretCtx); err != nil { - return err - } - } - return nil - } - return ctx.Write(newSecretContext(), render) -} - -func newSecretContext() *secretContext { - sCtx := &secretContext{} - - sCtx.header = map[string]string{ - "ID": secretIDHeader, - "Name": nameHeader, - "CreatedAt": secretCreatedHeader, - "UpdatedAt": secretUpdatedHeader, - "Labels": labelsHeader, - } - return sCtx -} - -type secretContext struct { - HeaderContext - s swarm.Secret -} - -func (c *secretContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *secretContext) ID() string { - return c.s.ID -} - -func (c *secretContext) Name() string { - return c.s.Spec.Annotations.Name -} - -func (c *secretContext) CreatedAt() string { - return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.CreatedAt)) + " ago" -} - -func (c *secretContext) UpdatedAt() string { - return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.UpdatedAt)) + " ago" -} - -func (c *secretContext) Labels() string { - mapLabels := c.s.Spec.Annotations.Labels - if mapLabels == nil { - return "" - } - var joinLabels []string - for k, v := range mapLabels { - joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) - } - return strings.Join(joinLabels, ",") -} - -func (c *secretContext) Label(name string) string { - if c.s.Spec.Annotations.Labels == nil { - return "" - } - return c.s.Spec.Annotations.Labels[name] -} diff --git a/cli/command/formatter/secret_test.go b/cli/command/formatter/secret_test.go deleted file mode 100644 index 98fe61315f..0000000000 --- a/cli/command/formatter/secret_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package formatter - -import ( - "bytes" - "testing" - "time" - - "github.com/docker/docker/api/types/swarm" - "github.com/stretchr/testify/assert" -) - -func TestSecretContextFormatWrite(t *testing.T) { - // Check default output format (verbose and non-verbose mode) for table headers - cases := []struct { - context Context - expected string - }{ - // Errors - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table format - {Context{Format: NewSecretFormat("table", false)}, - `ID NAME CREATED UPDATED -1 passwords Less than a second ago Less than a second ago -2 id_rsa Less than a second ago Less than a second ago -`}, - {Context{Format: NewSecretFormat("table {{.Name}}", true)}, - `NAME -passwords -id_rsa -`}, - {Context{Format: NewSecretFormat("{{.ID}}-{{.Name}}", false)}, - `1-passwords -2-id_rsa -`}, - } - - secrets := []swarm.Secret{ - {ID: "1", - Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()}, - Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "passwords"}}}, - {ID: "2", - Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()}, - Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "id_rsa"}}}, - } - for _, testcase := range cases { - out := bytes.NewBufferString("") - testcase.context.Output = out - if err := SecretWrite(testcase.context, secrets); err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} diff --git a/cli/command/formatter/service.go b/cli/command/formatter/service.go deleted file mode 100644 index e32704f337..0000000000 --- a/cli/command/formatter/service.go +++ /dev/null @@ -1,535 +0,0 @@ -package formatter - -import ( - "fmt" - "strings" - "time" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - mounttypes "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command/inspect" - "github.com/docker/docker/pkg/stringid" - units "github.com/docker/go-units" - "github.com/pkg/errors" -) - -const serviceInspectPrettyTemplate Format = ` -ID: {{.ID}} -Name: {{.Name}} -{{- if .Labels }} -Labels: -{{- range $k, $v := .Labels }} - {{ $k }}{{if $v }}={{ $v }}{{ end }} -{{- end }}{{ end }} -Service Mode: -{{- if .IsModeGlobal }} Global -{{- else if .IsModeReplicated }} Replicated -{{- if .ModeReplicatedReplicas }} - Replicas: {{ .ModeReplicatedReplicas }} -{{- end }}{{ end }} -{{- if .HasUpdateStatus }} -UpdateStatus: - State: {{ .UpdateStatusState }} -{{- if .HasUpdateStatusStarted }} - Started: {{ .UpdateStatusStarted }} -{{- end }} -{{- if .UpdateIsCompleted }} - Completed: {{ .UpdateStatusCompleted }} -{{- end }} - Message: {{ .UpdateStatusMessage }} -{{- end }} -Placement: -{{- if .TaskPlacementConstraints }} - Constraints: {{ .TaskPlacementConstraints }} -{{- end }} -{{- if .TaskPlacementPreferences }} - Preferences: {{ .TaskPlacementPreferences }} -{{- end }} -{{- if .HasUpdateConfig }} -UpdateConfig: - Parallelism: {{ .UpdateParallelism }} -{{- if .HasUpdateDelay}} - Delay: {{ .UpdateDelay }} -{{- end }} - On failure: {{ .UpdateOnFailure }} -{{- if .HasUpdateMonitor}} - Monitoring Period: {{ .UpdateMonitor }} -{{- end }} - Max failure ratio: {{ .UpdateMaxFailureRatio }} - Update order: {{ .UpdateOrder }} -{{- end }} -{{- if .HasRollbackConfig }} -RollbackConfig: - Parallelism: {{ .RollbackParallelism }} -{{- if .HasRollbackDelay}} - Delay: {{ .RollbackDelay }} -{{- end }} - On failure: {{ .RollbackOnFailure }} -{{- if .HasRollbackMonitor}} - Monitoring Period: {{ .RollbackMonitor }} -{{- end }} - Max failure ratio: {{ .RollbackMaxFailureRatio }} - Rollback order: {{ .RollbackOrder }} -{{- end }} -ContainerSpec: - Image: {{ .ContainerImage }} -{{- if .ContainerArgs }} - Args: {{ range $arg := .ContainerArgs }}{{ $arg }} {{ end }} -{{- end -}} -{{- if .ContainerEnv }} - Env: {{ range $env := .ContainerEnv }}{{ $env }} {{ end }} -{{- end -}} -{{- if .ContainerWorkDir }} - Dir: {{ .ContainerWorkDir }} -{{- end -}} -{{- if .ContainerUser }} - User: {{ .ContainerUser }} -{{- end }} -{{- if .ContainerMounts }} -Mounts: -{{- end }} -{{- range $mount := .ContainerMounts }} - Target = {{ $mount.Target }} - Source = {{ $mount.Source }} - ReadOnly = {{ $mount.ReadOnly }} - Type = {{ $mount.Type }} -{{- end -}} -{{- if .HasResources }} -Resources: -{{- if .HasResourceReservations }} - Reservations: -{{- if gt .ResourceReservationNanoCPUs 0.0 }} - CPU: {{ .ResourceReservationNanoCPUs }} -{{- end }} -{{- if .ResourceReservationMemory }} - Memory: {{ .ResourceReservationMemory }} -{{- end }}{{ end }} -{{- if .HasResourceLimits }} - Limits: -{{- if gt .ResourceLimitsNanoCPUs 0.0 }} - CPU: {{ .ResourceLimitsNanoCPUs }} -{{- end }} -{{- if .ResourceLimitMemory }} - Memory: {{ .ResourceLimitMemory }} -{{- end }}{{ end }}{{ end }} -{{- if .Networks }} -Networks: -{{- range $network := .Networks }} {{ $network }}{{ end }} {{ end }} -Endpoint Mode: {{ .EndpointMode }} -{{- if .Ports }} -Ports: -{{- range $port := .Ports }} - PublishedPort = {{ $port.PublishedPort }} - Protocol = {{ $port.Protocol }} - TargetPort = {{ $port.TargetPort }} - PublishMode = {{ $port.PublishMode }} -{{- end }} {{ end -}} -` - -// NewServiceFormat returns a Format for rendering using a Context -func NewServiceFormat(source string) Format { - switch source { - case PrettyFormatKey: - return serviceInspectPrettyTemplate - default: - return Format(strings.TrimPrefix(source, RawFormatKey)) - } -} - -func resolveNetworks(service swarm.Service, getNetwork inspect.GetRefFunc) map[string]string { - networkNames := make(map[string]string) - for _, network := range service.Spec.TaskTemplate.Networks { - if resolved, _, err := getNetwork(network.Target); err == nil { - if resolvedNetwork, ok := resolved.(types.NetworkResource); ok { - networkNames[resolvedNetwork.ID] = resolvedNetwork.Name - } - } - } - return networkNames -} - -// ServiceInspectWrite renders the context for a list of services -func ServiceInspectWrite(ctx Context, refs []string, getRef, getNetwork inspect.GetRefFunc) error { - if ctx.Format != serviceInspectPrettyTemplate { - return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) - } - render := func(format func(subContext subContext) error) error { - for _, ref := range refs { - serviceI, _, err := getRef(ref) - if err != nil { - return err - } - service, ok := serviceI.(swarm.Service) - if !ok { - return errors.Errorf("got wrong object to inspect") - } - if err := format(&serviceInspectContext{Service: service, networkNames: resolveNetworks(service, getNetwork)}); err != nil { - return err - } - } - return nil - } - return ctx.Write(&serviceInspectContext{}, render) -} - -type serviceInspectContext struct { - swarm.Service - subContext - - // networkNames is a map from network IDs (as found in - // Networks[x].Target) to network names. - networkNames map[string]string -} - -func (ctx *serviceInspectContext) MarshalJSON() ([]byte, error) { - return marshalJSON(ctx) -} - -func (ctx *serviceInspectContext) ID() string { - return ctx.Service.ID -} - -func (ctx *serviceInspectContext) Name() string { - return ctx.Service.Spec.Name -} - -func (ctx *serviceInspectContext) Labels() map[string]string { - return ctx.Service.Spec.Labels -} - -func (ctx *serviceInspectContext) IsModeGlobal() bool { - return ctx.Service.Spec.Mode.Global != nil -} - -func (ctx *serviceInspectContext) IsModeReplicated() bool { - return ctx.Service.Spec.Mode.Replicated != nil -} - -func (ctx *serviceInspectContext) ModeReplicatedReplicas() *uint64 { - return ctx.Service.Spec.Mode.Replicated.Replicas -} - -func (ctx *serviceInspectContext) HasUpdateStatus() bool { - return ctx.Service.UpdateStatus != nil && ctx.Service.UpdateStatus.State != "" -} - -func (ctx *serviceInspectContext) UpdateStatusState() swarm.UpdateState { - return ctx.Service.UpdateStatus.State -} - -func (ctx *serviceInspectContext) HasUpdateStatusStarted() bool { - return ctx.Service.UpdateStatus.StartedAt != nil -} - -func (ctx *serviceInspectContext) UpdateStatusStarted() string { - return units.HumanDuration(time.Since(*ctx.Service.UpdateStatus.StartedAt)) + " ago" -} - -func (ctx *serviceInspectContext) UpdateIsCompleted() bool { - return ctx.Service.UpdateStatus.State == swarm.UpdateStateCompleted && ctx.Service.UpdateStatus.CompletedAt != nil -} - -func (ctx *serviceInspectContext) UpdateStatusCompleted() string { - return units.HumanDuration(time.Since(*ctx.Service.UpdateStatus.CompletedAt)) + " ago" -} - -func (ctx *serviceInspectContext) UpdateStatusMessage() string { - return ctx.Service.UpdateStatus.Message -} - -func (ctx *serviceInspectContext) TaskPlacementConstraints() []string { - if ctx.Service.Spec.TaskTemplate.Placement != nil { - return ctx.Service.Spec.TaskTemplate.Placement.Constraints - } - return nil -} - -func (ctx *serviceInspectContext) TaskPlacementPreferences() []string { - if ctx.Service.Spec.TaskTemplate.Placement == nil { - return nil - } - var strings []string - for _, pref := range ctx.Service.Spec.TaskTemplate.Placement.Preferences { - if pref.Spread != nil { - strings = append(strings, "spread="+pref.Spread.SpreadDescriptor) - } - } - return strings -} - -func (ctx *serviceInspectContext) HasUpdateConfig() bool { - return ctx.Service.Spec.UpdateConfig != nil -} - -func (ctx *serviceInspectContext) UpdateParallelism() uint64 { - return ctx.Service.Spec.UpdateConfig.Parallelism -} - -func (ctx *serviceInspectContext) HasUpdateDelay() bool { - return ctx.Service.Spec.UpdateConfig.Delay.Nanoseconds() > 0 -} - -func (ctx *serviceInspectContext) UpdateDelay() time.Duration { - return ctx.Service.Spec.UpdateConfig.Delay -} - -func (ctx *serviceInspectContext) UpdateOnFailure() string { - return ctx.Service.Spec.UpdateConfig.FailureAction -} - -func (ctx *serviceInspectContext) UpdateOrder() string { - return ctx.Service.Spec.UpdateConfig.Order -} - -func (ctx *serviceInspectContext) HasUpdateMonitor() bool { - return ctx.Service.Spec.UpdateConfig.Monitor.Nanoseconds() > 0 -} - -func (ctx *serviceInspectContext) UpdateMonitor() time.Duration { - return ctx.Service.Spec.UpdateConfig.Monitor -} - -func (ctx *serviceInspectContext) UpdateMaxFailureRatio() float32 { - return ctx.Service.Spec.UpdateConfig.MaxFailureRatio -} - -func (ctx *serviceInspectContext) HasRollbackConfig() bool { - return ctx.Service.Spec.RollbackConfig != nil -} - -func (ctx *serviceInspectContext) RollbackParallelism() uint64 { - return ctx.Service.Spec.RollbackConfig.Parallelism -} - -func (ctx *serviceInspectContext) HasRollbackDelay() bool { - return ctx.Service.Spec.RollbackConfig.Delay.Nanoseconds() > 0 -} - -func (ctx *serviceInspectContext) RollbackDelay() time.Duration { - return ctx.Service.Spec.RollbackConfig.Delay -} - -func (ctx *serviceInspectContext) RollbackOnFailure() string { - return ctx.Service.Spec.RollbackConfig.FailureAction -} - -func (ctx *serviceInspectContext) HasRollbackMonitor() bool { - return ctx.Service.Spec.RollbackConfig.Monitor.Nanoseconds() > 0 -} - -func (ctx *serviceInspectContext) RollbackMonitor() time.Duration { - return ctx.Service.Spec.RollbackConfig.Monitor -} - -func (ctx *serviceInspectContext) RollbackMaxFailureRatio() float32 { - return ctx.Service.Spec.RollbackConfig.MaxFailureRatio -} - -func (ctx *serviceInspectContext) RollbackOrder() string { - return ctx.Service.Spec.RollbackConfig.Order -} - -func (ctx *serviceInspectContext) ContainerImage() string { - return ctx.Service.Spec.TaskTemplate.ContainerSpec.Image -} - -func (ctx *serviceInspectContext) ContainerArgs() []string { - return ctx.Service.Spec.TaskTemplate.ContainerSpec.Args -} - -func (ctx *serviceInspectContext) ContainerEnv() []string { - return ctx.Service.Spec.TaskTemplate.ContainerSpec.Env -} - -func (ctx *serviceInspectContext) ContainerWorkDir() string { - return ctx.Service.Spec.TaskTemplate.ContainerSpec.Dir -} - -func (ctx *serviceInspectContext) ContainerUser() string { - return ctx.Service.Spec.TaskTemplate.ContainerSpec.User -} - -func (ctx *serviceInspectContext) ContainerMounts() []mounttypes.Mount { - return ctx.Service.Spec.TaskTemplate.ContainerSpec.Mounts -} - -func (ctx *serviceInspectContext) HasResources() bool { - return ctx.Service.Spec.TaskTemplate.Resources != nil -} - -func (ctx *serviceInspectContext) HasResourceReservations() bool { - if ctx.Service.Spec.TaskTemplate.Resources == nil || ctx.Service.Spec.TaskTemplate.Resources.Reservations == nil { - return false - } - return ctx.Service.Spec.TaskTemplate.Resources.Reservations.NanoCPUs > 0 || ctx.Service.Spec.TaskTemplate.Resources.Reservations.MemoryBytes > 0 -} - -func (ctx *serviceInspectContext) ResourceReservationNanoCPUs() float64 { - if ctx.Service.Spec.TaskTemplate.Resources.Reservations.NanoCPUs == 0 { - return float64(0) - } - return float64(ctx.Service.Spec.TaskTemplate.Resources.Reservations.NanoCPUs) / 1e9 -} - -func (ctx *serviceInspectContext) ResourceReservationMemory() string { - if ctx.Service.Spec.TaskTemplate.Resources.Reservations.MemoryBytes == 0 { - return "" - } - return units.BytesSize(float64(ctx.Service.Spec.TaskTemplate.Resources.Reservations.MemoryBytes)) -} - -func (ctx *serviceInspectContext) HasResourceLimits() bool { - if ctx.Service.Spec.TaskTemplate.Resources == nil || ctx.Service.Spec.TaskTemplate.Resources.Limits == nil { - return false - } - return ctx.Service.Spec.TaskTemplate.Resources.Limits.NanoCPUs > 0 || ctx.Service.Spec.TaskTemplate.Resources.Limits.MemoryBytes > 0 -} - -func (ctx *serviceInspectContext) ResourceLimitsNanoCPUs() float64 { - return float64(ctx.Service.Spec.TaskTemplate.Resources.Limits.NanoCPUs) / 1e9 -} - -func (ctx *serviceInspectContext) ResourceLimitMemory() string { - if ctx.Service.Spec.TaskTemplate.Resources.Limits.MemoryBytes == 0 { - return "" - } - return units.BytesSize(float64(ctx.Service.Spec.TaskTemplate.Resources.Limits.MemoryBytes)) -} - -func (ctx *serviceInspectContext) Networks() []string { - var out []string - for _, n := range ctx.Service.Spec.TaskTemplate.Networks { - if name, ok := ctx.networkNames[n.Target]; ok { - out = append(out, name) - } else { - out = append(out, n.Target) - } - } - return out -} - -func (ctx *serviceInspectContext) EndpointMode() string { - if ctx.Service.Spec.EndpointSpec == nil { - return "" - } - - return string(ctx.Service.Spec.EndpointSpec.Mode) -} - -func (ctx *serviceInspectContext) Ports() []swarm.PortConfig { - return ctx.Service.Endpoint.Ports -} - -const ( - defaultServiceTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Mode}}\t{{.Replicas}}\t{{.Image}}\t{{.Ports}}" - - serviceIDHeader = "ID" - modeHeader = "MODE" - replicasHeader = "REPLICAS" -) - -// NewServiceListFormat returns a Format for rendering using a service Context -func NewServiceListFormat(source string, quiet bool) Format { - switch source { - case TableFormatKey: - if quiet { - return defaultQuietFormat - } - return defaultServiceTableFormat - case RawFormatKey: - if quiet { - return `id: {{.ID}}` - } - return `id: {{.ID}}\nname: {{.Name}}\nmode: {{.Mode}}\nreplicas: {{.Replicas}}\nimage: {{.Image}}\nports: {{.Ports}}\n` - } - return Format(source) -} - -// ServiceListInfo stores the information about mode and replicas to be used by template -type ServiceListInfo struct { - Mode string - Replicas string -} - -// ServiceListWrite writes the context -func ServiceListWrite(ctx Context, services []swarm.Service, info map[string]ServiceListInfo) error { - render := func(format func(subContext subContext) error) error { - for _, service := range services { - serviceCtx := &serviceContext{service: service, mode: info[service.ID].Mode, replicas: info[service.ID].Replicas} - if err := format(serviceCtx); err != nil { - return err - } - } - return nil - } - serviceCtx := serviceContext{} - serviceCtx.header = map[string]string{ - "ID": serviceIDHeader, - "Name": nameHeader, - "Mode": modeHeader, - "Replicas": replicasHeader, - "Image": imageHeader, - "Ports": portsHeader, - } - return ctx.Write(&serviceCtx, render) -} - -type serviceContext struct { - HeaderContext - service swarm.Service - mode string - replicas string -} - -func (c *serviceContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *serviceContext) ID() string { - return stringid.TruncateID(c.service.ID) -} - -func (c *serviceContext) Name() string { - return c.service.Spec.Name -} - -func (c *serviceContext) Mode() string { - return c.mode -} - -func (c *serviceContext) Replicas() string { - return c.replicas -} - -func (c *serviceContext) Image() string { - image := c.service.Spec.TaskTemplate.ContainerSpec.Image - if ref, err := reference.ParseNormalizedNamed(image); err == nil { - // update image string for display, (strips any digest) - if nt, ok := ref.(reference.NamedTagged); ok { - if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil { - image = reference.FamiliarString(namedTagged) - } - } - } - - return image -} - -func (c *serviceContext) Ports() string { - if c.service.Spec.EndpointSpec == nil || c.service.Spec.EndpointSpec.Ports == nil { - return "" - } - ports := []string{} - for _, pConfig := range c.service.Spec.EndpointSpec.Ports { - if pConfig.PublishMode == swarm.PortConfigPublishModeIngress { - ports = append(ports, fmt.Sprintf("*:%d->%d/%s", - pConfig.PublishedPort, - pConfig.TargetPort, - pConfig.Protocol, - )) - } - } - return strings.Join(ports, ",") -} diff --git a/cli/command/formatter/service_test.go b/cli/command/formatter/service_test.go deleted file mode 100644 index 629f853930..0000000000 --- a/cli/command/formatter/service_test.go +++ /dev/null @@ -1,239 +0,0 @@ -package formatter - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - - "github.com/docker/docker/api/types/swarm" - "github.com/stretchr/testify/assert" -) - -func TestServiceContextWrite(t *testing.T) { - cases := []struct { - context Context - expected string - }{ - // Errors - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table format - { - Context{Format: NewServiceListFormat("table", false)}, - `ID NAME MODE REPLICAS IMAGE PORTS -id_baz baz global 2/4 *:80->8080/tcp -id_bar bar replicated 2/4 *:80->8080/tcp -`, - }, - { - Context{Format: NewServiceListFormat("table", true)}, - `id_baz -id_bar -`, - }, - { - Context{Format: NewServiceListFormat("table {{.Name}}", false)}, - `NAME -baz -bar -`, - }, - { - Context{Format: NewServiceListFormat("table {{.Name}}", true)}, - `NAME -baz -bar -`, - }, - // Raw Format - { - Context{Format: NewServiceListFormat("raw", false)}, - `id: id_baz -name: baz -mode: global -replicas: 2/4 -image: -ports: *:80->8080/tcp - -id: id_bar -name: bar -mode: replicated -replicas: 2/4 -image: -ports: *:80->8080/tcp - -`, - }, - { - Context{Format: NewServiceListFormat("raw", true)}, - `id: id_baz -id: id_bar -`, - }, - // Custom Format - { - Context{Format: NewServiceListFormat("{{.Name}}", false)}, - `baz -bar -`, - }, - } - - for _, testcase := range cases { - services := []swarm.Service{ - { - ID: "id_baz", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "baz"}, - EndpointSpec: &swarm.EndpointSpec{ - Ports: []swarm.PortConfig{ - { - PublishMode: "ingress", - PublishedPort: 80, - TargetPort: 8080, - Protocol: "tcp", - }, - }, - }, - }, - }, - { - ID: "id_bar", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "bar"}, - EndpointSpec: &swarm.EndpointSpec{ - Ports: []swarm.PortConfig{ - { - PublishMode: "ingress", - PublishedPort: 80, - TargetPort: 8080, - Protocol: "tcp", - }, - }, - }, - }, - }, - } - info := map[string]ServiceListInfo{ - "id_baz": { - Mode: "global", - Replicas: "2/4", - }, - "id_bar": { - Mode: "replicated", - Replicas: "2/4", - }, - } - out := bytes.NewBufferString("") - testcase.context.Output = out - err := ServiceListWrite(testcase.context, services, info) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} - -func TestServiceContextWriteJSON(t *testing.T) { - services := []swarm.Service{ - { - ID: "id_baz", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "baz"}, - EndpointSpec: &swarm.EndpointSpec{ - Ports: []swarm.PortConfig{ - { - PublishMode: "ingress", - PublishedPort: 80, - TargetPort: 8080, - Protocol: "tcp", - }, - }, - }, - }, - }, - { - ID: "id_bar", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "bar"}, - EndpointSpec: &swarm.EndpointSpec{ - Ports: []swarm.PortConfig{ - { - PublishMode: "ingress", - PublishedPort: 80, - TargetPort: 8080, - Protocol: "tcp", - }, - }, - }, - }, - }, - } - info := map[string]ServiceListInfo{ - "id_baz": { - Mode: "global", - Replicas: "2/4", - }, - "id_bar": { - Mode: "replicated", - Replicas: "2/4", - }, - } - expectedJSONs := []map[string]interface{}{ - {"ID": "id_baz", "Name": "baz", "Mode": "global", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"}, - {"ID": "id_bar", "Name": "bar", "Mode": "replicated", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"}, - } - - out := bytes.NewBufferString("") - err := ServiceListWrite(Context{Format: "{{json .}}", Output: out}, services, info) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var m map[string]interface{} - if err := json.Unmarshal([]byte(line), &m); err != nil { - t.Fatal(err) - } - assert.Equal(t, expectedJSONs[i], m) - } -} -func TestServiceContextWriteJSONField(t *testing.T) { - services := []swarm.Service{ - {ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}}, - {ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}}, - } - info := map[string]ServiceListInfo{ - "id_baz": { - Mode: "global", - Replicas: "2/4", - }, - "id_bar": { - Mode: "replicated", - Replicas: "2/4", - }, - } - out := bytes.NewBufferString("") - err := ServiceListWrite(Context{Format: "{{json .Name}}", Output: out}, services, info) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var s string - if err := json.Unmarshal([]byte(line), &s); err != nil { - t.Fatal(err) - } - assert.Equal(t, services[i].Spec.Name, s) - } -} diff --git a/cli/command/formatter/stack.go b/cli/command/formatter/stack.go deleted file mode 100644 index 676bcc05fe..0000000000 --- a/cli/command/formatter/stack.go +++ /dev/null @@ -1,67 +0,0 @@ -package formatter - -import ( - "strconv" -) - -const ( - defaultStackTableFormat = "table {{.Name}}\t{{.Services}}" - - stackServicesHeader = "SERVICES" -) - -// Stack contains deployed stack information. -type Stack struct { - // Name is the name of the stack - Name string - // Services is the number of the services - Services int -} - -// NewStackFormat returns a format for use with a stack Context -func NewStackFormat(source string) Format { - switch source { - case TableFormatKey: - return defaultStackTableFormat - } - return Format(source) -} - -// StackWrite writes formatted stacks using the Context -func StackWrite(ctx Context, stacks []*Stack) error { - render := func(format func(subContext subContext) error) error { - for _, stack := range stacks { - if err := format(&stackContext{s: stack}); err != nil { - return err - } - } - return nil - } - return ctx.Write(newStackContext(), render) -} - -type stackContext struct { - HeaderContext - s *Stack -} - -func newStackContext() *stackContext { - stackCtx := stackContext{} - stackCtx.header = map[string]string{ - "Name": nameHeader, - "Services": stackServicesHeader, - } - return &stackCtx -} - -func (s *stackContext) MarshalJSON() ([]byte, error) { - return marshalJSON(s) -} - -func (s *stackContext) Name() string { - return s.s.Name -} - -func (s *stackContext) Services() string { - return strconv.Itoa(s.s.Services) -} diff --git a/cli/command/formatter/stack_test.go b/cli/command/formatter/stack_test.go deleted file mode 100644 index b18ae7f083..0000000000 --- a/cli/command/formatter/stack_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package formatter - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestStackContextWrite(t *testing.T) { - cases := []struct { - context Context - expected string - }{ - // Errors - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table format - { - Context{Format: NewStackFormat("table")}, - `NAME SERVICES -baz 2 -bar 1 -`, - }, - { - Context{Format: NewStackFormat("table {{.Name}}")}, - `NAME -baz -bar -`, - }, - // Custom Format - { - Context{Format: NewStackFormat("{{.Name}}")}, - `baz -bar -`, - }, - } - - stacks := []*Stack{ - {Name: "baz", Services: 2}, - {Name: "bar", Services: 1}, - } - for _, testcase := range cases { - out := bytes.NewBufferString("") - testcase.context.Output = out - err := StackWrite(testcase.context, stacks) - if err != nil { - assert.Error(t, err, testcase.expected) - } else { - assert.Equal(t, out.String(), testcase.expected) - } - } -} diff --git a/cli/command/formatter/stats.go b/cli/command/formatter/stats.go deleted file mode 100644 index c0151101a0..0000000000 --- a/cli/command/formatter/stats.go +++ /dev/null @@ -1,220 +0,0 @@ -package formatter - -import ( - "fmt" - "sync" - - units "github.com/docker/go-units" -) - -const ( - winOSType = "windows" - defaultStatsTableFormat = "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}" - winDefaultStatsTableFormat = "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}" - - containerHeader = "CONTAINER" - cpuPercHeader = "CPU %" - netIOHeader = "NET I/O" - blockIOHeader = "BLOCK I/O" - memPercHeader = "MEM %" // Used only on Linux - winMemUseHeader = "PRIV WORKING SET" // Used only on Windows - memUseHeader = "MEM USAGE / LIMIT" // Used only on Linux - pidsHeader = "PIDS" // Used only on Linux -) - -// StatsEntry represents represents the statistics data collected from a container -type StatsEntry struct { - Container string - Name string - ID string - CPUPercentage float64 - Memory float64 // On Windows this is the private working set - MemoryLimit float64 // Not used on Windows - MemoryPercentage float64 // Not used on Windows - NetworkRx float64 - NetworkTx float64 - BlockRead float64 - BlockWrite float64 - PidsCurrent uint64 // Not used on Windows - IsInvalid bool -} - -// ContainerStats represents an entity to store containers statistics synchronously -type ContainerStats struct { - mutex sync.Mutex - StatsEntry - err error -} - -// GetError returns the container statistics error. -// This is used to determine whether the statistics are valid or not -func (cs *ContainerStats) GetError() error { - cs.mutex.Lock() - defer cs.mutex.Unlock() - return cs.err -} - -// SetErrorAndReset zeroes all the container statistics and store the error. -// It is used when receiving time out error during statistics collecting to reduce lock overhead -func (cs *ContainerStats) SetErrorAndReset(err error) { - cs.mutex.Lock() - defer cs.mutex.Unlock() - cs.CPUPercentage = 0 - cs.Memory = 0 - cs.MemoryPercentage = 0 - cs.MemoryLimit = 0 - cs.NetworkRx = 0 - cs.NetworkTx = 0 - cs.BlockRead = 0 - cs.BlockWrite = 0 - cs.PidsCurrent = 0 - cs.err = err - cs.IsInvalid = true -} - -// SetError sets container statistics error -func (cs *ContainerStats) SetError(err error) { - cs.mutex.Lock() - defer cs.mutex.Unlock() - cs.err = err - if err != nil { - cs.IsInvalid = true - } -} - -// SetStatistics set the container statistics -func (cs *ContainerStats) SetStatistics(s StatsEntry) { - cs.mutex.Lock() - defer cs.mutex.Unlock() - s.Container = cs.Container - cs.StatsEntry = s -} - -// GetStatistics returns container statistics with other meta data such as the container name -func (cs *ContainerStats) GetStatistics() StatsEntry { - cs.mutex.Lock() - defer cs.mutex.Unlock() - return cs.StatsEntry -} - -// NewStatsFormat returns a format for rendering an CStatsContext -func NewStatsFormat(source, osType string) Format { - if source == TableFormatKey { - if osType == winOSType { - return Format(winDefaultStatsTableFormat) - } - return Format(defaultStatsTableFormat) - } - return Format(source) -} - -// NewContainerStats returns a new ContainerStats entity and sets in it the given name -func NewContainerStats(container, osType string) *ContainerStats { - return &ContainerStats{ - StatsEntry: StatsEntry{Container: container}, - } -} - -// ContainerStatsWrite renders the context for a list of containers statistics -func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string) error { - render := func(format func(subContext subContext) error) error { - for _, cstats := range containerStats { - containerStatsCtx := &containerStatsContext{ - s: cstats, - os: osType, - } - if err := format(containerStatsCtx); err != nil { - return err - } - } - return nil - } - memUsage := memUseHeader - if osType == winOSType { - memUsage = winMemUseHeader - } - containerStatsCtx := containerStatsContext{} - containerStatsCtx.header = map[string]string{ - "Container": containerHeader, - "Name": nameHeader, - "ID": containerIDHeader, - "CPUPerc": cpuPercHeader, - "MemUsage": memUsage, - "MemPerc": memPercHeader, - "NetIO": netIOHeader, - "BlockIO": blockIOHeader, - "PIDs": pidsHeader, - } - containerStatsCtx.os = osType - return ctx.Write(&containerStatsCtx, render) -} - -type containerStatsContext struct { - HeaderContext - s StatsEntry - os string -} - -func (c *containerStatsContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *containerStatsContext) Container() string { - return c.s.Container -} - -func (c *containerStatsContext) Name() string { - if len(c.s.Name) > 1 { - return c.s.Name[1:] - } - return "--" -} - -func (c *containerStatsContext) ID() string { - return c.s.ID -} - -func (c *containerStatsContext) CPUPerc() string { - if c.s.IsInvalid { - return fmt.Sprintf("--") - } - return fmt.Sprintf("%.2f%%", c.s.CPUPercentage) -} - -func (c *containerStatsContext) MemUsage() string { - if c.s.IsInvalid { - return fmt.Sprintf("-- / --") - } - if c.os == winOSType { - return fmt.Sprintf("%s", units.BytesSize(c.s.Memory)) - } - return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit)) -} - -func (c *containerStatsContext) MemPerc() string { - if c.s.IsInvalid || c.os == winOSType { - return fmt.Sprintf("--") - } - return fmt.Sprintf("%.2f%%", c.s.MemoryPercentage) -} - -func (c *containerStatsContext) NetIO() string { - if c.s.IsInvalid { - return fmt.Sprintf("--") - } - return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.NetworkRx, 3), units.HumanSizeWithPrecision(c.s.NetworkTx, 3)) -} - -func (c *containerStatsContext) BlockIO() string { - if c.s.IsInvalid { - return fmt.Sprintf("--") - } - return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.BlockRead, 3), units.HumanSizeWithPrecision(c.s.BlockWrite, 3)) -} - -func (c *containerStatsContext) PIDs() string { - if c.s.IsInvalid || c.os == winOSType { - return fmt.Sprintf("--") - } - return fmt.Sprintf("%d", c.s.PidsCurrent) -} diff --git a/cli/command/formatter/stats_test.go b/cli/command/formatter/stats_test.go deleted file mode 100644 index 078e8db32a..0000000000 --- a/cli/command/formatter/stats_test.go +++ /dev/null @@ -1,266 +0,0 @@ -package formatter - -import ( - "bytes" - "testing" - - "github.com/docker/docker/pkg/stringid" - "github.com/stretchr/testify/assert" -) - -func TestContainerStatsContext(t *testing.T) { - containerID := stringid.GenerateRandomID() - - var ctx containerStatsContext - tt := []struct { - stats StatsEntry - osType string - expValue string - expHeader string - call func() string - }{ - {StatsEntry{Container: containerID}, "", containerID, containerHeader, ctx.Container}, - {StatsEntry{CPUPercentage: 5.5}, "", "5.50%", cpuPercHeader, ctx.CPUPerc}, - {StatsEntry{CPUPercentage: 5.5, IsInvalid: true}, "", "--", cpuPercHeader, ctx.CPUPerc}, - {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3}, "", "0.31B / 12.3B", netIOHeader, ctx.NetIO}, - {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3, IsInvalid: true}, "", "--", netIOHeader, ctx.NetIO}, - {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3}, "", "0.1B / 2.3B", blockIOHeader, ctx.BlockIO}, - {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3, IsInvalid: true}, "", "--", blockIOHeader, ctx.BlockIO}, - {StatsEntry{MemoryPercentage: 10.2}, "", "10.20%", memPercHeader, ctx.MemPerc}, - {StatsEntry{MemoryPercentage: 10.2, IsInvalid: true}, "", "--", memPercHeader, ctx.MemPerc}, - {StatsEntry{MemoryPercentage: 10.2}, "windows", "--", memPercHeader, ctx.MemPerc}, - {StatsEntry{Memory: 24, MemoryLimit: 30}, "", "24B / 30B", memUseHeader, ctx.MemUsage}, - {StatsEntry{Memory: 24, MemoryLimit: 30, IsInvalid: true}, "", "-- / --", memUseHeader, ctx.MemUsage}, - {StatsEntry{Memory: 24, MemoryLimit: 30}, "windows", "24B", winMemUseHeader, ctx.MemUsage}, - {StatsEntry{PidsCurrent: 10}, "", "10", pidsHeader, ctx.PIDs}, - {StatsEntry{PidsCurrent: 10, IsInvalid: true}, "", "--", pidsHeader, ctx.PIDs}, - {StatsEntry{PidsCurrent: 10}, "windows", "--", pidsHeader, ctx.PIDs}, - } - - for _, te := range tt { - ctx = containerStatsContext{s: te.stats, os: te.osType} - if v := te.call(); v != te.expValue { - t.Fatalf("Expected %q, got %q", te.expValue, v) - } - } -} - -func TestContainerStatsContextWrite(t *testing.T) { - tt := []struct { - context Context - expected string - }{ - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - { - Context{Format: "table {{.MemUsage}}"}, - `MEM USAGE / LIMIT -20B / 20B --- / -- -`, - }, - { - Context{Format: "{{.Container}} {{.ID}} {{.Name}}"}, - `container1 abcdef foo -container2 -- -`, - }, - { - Context{Format: "{{.Container}} {{.CPUPerc}}"}, - `container1 20.00% -container2 -- -`, - }, - } - - for _, te := range tt { - stats := []StatsEntry{ - { - Container: "container1", - ID: "abcdef", - Name: "/foo", - CPUPercentage: 20, - Memory: 20, - MemoryLimit: 20, - MemoryPercentage: 20, - NetworkRx: 20, - NetworkTx: 20, - BlockRead: 20, - BlockWrite: 20, - PidsCurrent: 2, - IsInvalid: false, - }, - { - Container: "container2", - CPUPercentage: 30, - Memory: 30, - MemoryLimit: 30, - MemoryPercentage: 30, - NetworkRx: 30, - NetworkTx: 30, - BlockRead: 30, - BlockWrite: 30, - PidsCurrent: 3, - IsInvalid: true, - }, - } - var out bytes.Buffer - te.context.Output = &out - err := ContainerStatsWrite(te.context, stats, "linux") - if err != nil { - assert.EqualError(t, err, te.expected) - } else { - assert.Equal(t, te.expected, out.String()) - } - } -} - -func TestContainerStatsContextWriteWindows(t *testing.T) { - tt := []struct { - context Context - expected string - }{ - { - Context{Format: "table {{.MemUsage}}"}, - `PRIV WORKING SET -20B --- / -- -`, - }, - { - Context{Format: "{{.Container}} {{.CPUPerc}}"}, - `container1 20.00% -container2 -- -`, - }, - { - Context{Format: "{{.Container}} {{.MemPerc}} {{.PIDs}}"}, - `container1 -- -- -container2 -- -- -`, - }, - } - - for _, te := range tt { - stats := []StatsEntry{ - { - Container: "container1", - CPUPercentage: 20, - Memory: 20, - MemoryLimit: 20, - MemoryPercentage: 20, - NetworkRx: 20, - NetworkTx: 20, - BlockRead: 20, - BlockWrite: 20, - PidsCurrent: 2, - IsInvalid: false, - }, - { - Container: "container2", - CPUPercentage: 30, - Memory: 30, - MemoryLimit: 30, - MemoryPercentage: 30, - NetworkRx: 30, - NetworkTx: 30, - BlockRead: 30, - BlockWrite: 30, - PidsCurrent: 3, - IsInvalid: true, - }, - } - var out bytes.Buffer - te.context.Output = &out - err := ContainerStatsWrite(te.context, stats, "windows") - if err != nil { - assert.EqualError(t, err, te.expected) - } else { - assert.Equal(t, te.expected, out.String()) - } - } -} - -func TestContainerStatsContextWriteWithNoStats(t *testing.T) { - var out bytes.Buffer - - contexts := []struct { - context Context - expected string - }{ - { - Context{ - Format: "{{.Container}}", - Output: &out, - }, - "", - }, - { - Context{ - Format: "table {{.Container}}", - Output: &out, - }, - "CONTAINER\n", - }, - { - Context{ - Format: "table {{.Container}}\t{{.CPUPerc}}", - Output: &out, - }, - "CONTAINER CPU %\n", - }, - } - - for _, context := range contexts { - ContainerStatsWrite(context.context, []StatsEntry{}, "linux") - assert.Equal(t, context.expected, out.String()) - // Clean buffer - out.Reset() - } -} - -func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) { - var out bytes.Buffer - - contexts := []struct { - context Context - expected string - }{ - { - Context{ - Format: "{{.Container}}", - Output: &out, - }, - "", - }, - { - Context{ - Format: "table {{.Container}}\t{{.MemUsage}}", - Output: &out, - }, - "CONTAINER PRIV WORKING SET\n", - }, - { - Context{ - Format: "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}", - Output: &out, - }, - "CONTAINER CPU % PRIV WORKING SET\n", - }, - } - - for _, context := range contexts { - ContainerStatsWrite(context.context, []StatsEntry{}, "windows") - assert.Equal(t, context.expected, out.String()) - // Clean buffer - out.Reset() - } -} diff --git a/cli/command/formatter/task.go b/cli/command/formatter/task.go deleted file mode 100644 index 2c6e7bb124..0000000000 --- a/cli/command/formatter/task.go +++ /dev/null @@ -1,150 +0,0 @@ -package formatter - -import ( - "fmt" - "strings" - "time" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/go-units" -) - -const ( - defaultTaskTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Image}}\t{{.Node}}\t{{.DesiredState}}\t{{.CurrentState}}\t{{.Error}}\t{{.Ports}}" - - nodeHeader = "NODE" - taskIDHeader = "ID" - desiredStateHeader = "DESIRED STATE" - currentStateHeader = "CURRENT STATE" - errorHeader = "ERROR" - - maxErrLength = 30 -) - -// NewTaskFormat returns a Format for rendering using a task Context -func NewTaskFormat(source string, quiet bool) Format { - switch source { - case TableFormatKey: - if quiet { - return defaultQuietFormat - } - return defaultTaskTableFormat - case RawFormatKey: - if quiet { - return `id: {{.ID}}` - } - return `id: {{.ID}}\nname: {{.Name}}\nimage: {{.Image}}\nnode: {{.Node}}\ndesired_state: {{.DesiredState}}\ncurrent_state: {{.CurrentState}}\nerror: {{.Error}}\nports: {{.Ports}}\n` - } - return Format(source) -} - -// TaskWrite writes the context -func TaskWrite(ctx Context, tasks []swarm.Task, names map[string]string, nodes map[string]string) error { - render := func(format func(subContext subContext) error) error { - for _, task := range tasks { - taskCtx := &taskContext{trunc: ctx.Trunc, task: task, name: names[task.ID], node: nodes[task.ID]} - if err := format(taskCtx); err != nil { - return err - } - } - return nil - } - taskCtx := taskContext{} - taskCtx.header = taskHeaderContext{ - "ID": taskIDHeader, - "Name": nameHeader, - "Image": imageHeader, - "Node": nodeHeader, - "DesiredState": desiredStateHeader, - "CurrentState": currentStateHeader, - "Error": errorHeader, - "Ports": portsHeader, - } - return ctx.Write(&taskCtx, render) -} - -type taskHeaderContext map[string]string - -type taskContext struct { - HeaderContext - trunc bool - task swarm.Task - name string - node string -} - -func (c *taskContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *taskContext) ID() string { - if c.trunc { - return stringid.TruncateID(c.task.ID) - } - return c.task.ID -} - -func (c *taskContext) Name() string { - return c.name -} - -func (c *taskContext) Image() string { - image := c.task.Spec.ContainerSpec.Image - if c.trunc { - ref, err := reference.ParseNormalizedNamed(image) - if err == nil { - // update image string for display, (strips any digest) - if nt, ok := ref.(reference.NamedTagged); ok { - if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil { - image = reference.FamiliarString(namedTagged) - } - } - } - } - return image -} - -func (c *taskContext) Node() string { - return c.node -} - -func (c *taskContext) DesiredState() string { - return command.PrettyPrint(c.task.DesiredState) -} - -func (c *taskContext) CurrentState() string { - return fmt.Sprintf("%s %s ago", - command.PrettyPrint(c.task.Status.State), - strings.ToLower(units.HumanDuration(time.Since(c.task.Status.Timestamp))), - ) -} - -func (c *taskContext) Error() string { - // Trim and quote the error message. - taskErr := c.task.Status.Err - if c.trunc && len(taskErr) > maxErrLength { - taskErr = fmt.Sprintf("%s…", taskErr[:maxErrLength-1]) - } - if len(taskErr) > 0 { - taskErr = fmt.Sprintf("\"%s\"", taskErr) - } - return taskErr -} - -func (c *taskContext) Ports() string { - if len(c.task.Status.PortStatus.Ports) == 0 { - return "" - } - ports := []string{} - for _, pConfig := range c.task.Status.PortStatus.Ports { - ports = append(ports, fmt.Sprintf("*:%d->%d/%s", - pConfig.PublishedPort, - pConfig.TargetPort, - pConfig.Protocol, - )) - } - return strings.Join(ports, ",") -} diff --git a/cli/command/formatter/task_test.go b/cli/command/formatter/task_test.go deleted file mode 100644 index d2843c70d4..0000000000 --- a/cli/command/formatter/task_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package formatter - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - - "github.com/docker/docker/api/types/swarm" - "github.com/stretchr/testify/assert" -) - -func TestTaskContextWrite(t *testing.T) { - cases := []struct { - context Context - expected string - }{ - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - { - Context{Format: NewTaskFormat("table", true)}, - `taskID1 -taskID2 -`, - }, - { - Context{Format: NewTaskFormat("table {{.Name}}\t{{.Node}}\t{{.Ports}}", false)}, - `NAME NODE PORTS -foobar_baz foo1 -foobar_bar foo2 -`, - }, - { - Context{Format: NewTaskFormat("table {{.Name}}", true)}, - `NAME -foobar_baz -foobar_bar -`, - }, - { - Context{Format: NewTaskFormat("raw", true)}, - `id: taskID1 -id: taskID2 -`, - }, - { - Context{Format: NewTaskFormat("{{.Name}} {{.Node}}", false)}, - `foobar_baz foo1 -foobar_bar foo2 -`, - }, - } - - for _, testcase := range cases { - tasks := []swarm.Task{ - {ID: "taskID1"}, - {ID: "taskID2"}, - } - names := map[string]string{ - "taskID1": "foobar_baz", - "taskID2": "foobar_bar", - } - nodes := map[string]string{ - "taskID1": "foo1", - "taskID2": "foo2", - } - out := bytes.NewBufferString("") - testcase.context.Output = out - err := TaskWrite(testcase.context, tasks, names, nodes) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} - -func TestTaskContextWriteJSONField(t *testing.T) { - tasks := []swarm.Task{ - {ID: "taskID1"}, - {ID: "taskID2"}, - } - names := map[string]string{ - "taskID1": "foobar_baz", - "taskID2": "foobar_bar", - } - out := bytes.NewBufferString("") - err := TaskWrite(Context{Format: "{{json .ID}}", Output: out}, tasks, names, map[string]string{}) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - var s string - if err := json.Unmarshal([]byte(line), &s); err != nil { - t.Fatal(err) - } - assert.Equal(t, tasks[i].ID, s) - } -} diff --git a/cli/command/formatter/volume.go b/cli/command/formatter/volume.go deleted file mode 100644 index 342f2fb934..0000000000 --- a/cli/command/formatter/volume.go +++ /dev/null @@ -1,131 +0,0 @@ -package formatter - -import ( - "fmt" - "strings" - - "github.com/docker/docker/api/types" - units "github.com/docker/go-units" -) - -const ( - defaultVolumeQuietFormat = "{{.Name}}" - defaultVolumeTableFormat = "table {{.Driver}}\t{{.Name}}" - - volumeNameHeader = "VOLUME NAME" - mountpointHeader = "MOUNTPOINT" - linksHeader = "LINKS" - // Status header ? -) - -// NewVolumeFormat returns a format for use with a volume Context -func NewVolumeFormat(source string, quiet bool) Format { - switch source { - case TableFormatKey: - if quiet { - return defaultVolumeQuietFormat - } - return defaultVolumeTableFormat - case RawFormatKey: - if quiet { - return `name: {{.Name}}` - } - return `name: {{.Name}}\ndriver: {{.Driver}}\n` - } - return Format(source) -} - -// VolumeWrite writes formatted volumes using the Context -func VolumeWrite(ctx Context, volumes []*types.Volume) error { - render := func(format func(subContext subContext) error) error { - for _, volume := range volumes { - if err := format(&volumeContext{v: *volume}); err != nil { - return err - } - } - return nil - } - return ctx.Write(newVolumeContext(), render) -} - -type volumeHeaderContext map[string]string - -func (c volumeHeaderContext) Label(name string) string { - n := strings.Split(name, ".") - r := strings.NewReplacer("-", " ", "_", " ") - h := r.Replace(n[len(n)-1]) - - return h -} - -type volumeContext struct { - HeaderContext - v types.Volume -} - -func newVolumeContext() *volumeContext { - volumeCtx := volumeContext{} - volumeCtx.header = volumeHeaderContext{ - "Name": volumeNameHeader, - "Driver": driverHeader, - "Scope": scopeHeader, - "Mountpoint": mountpointHeader, - "Labels": labelsHeader, - "Links": linksHeader, - "Size": sizeHeader, - } - return &volumeCtx -} - -func (c *volumeContext) MarshalJSON() ([]byte, error) { - return marshalJSON(c) -} - -func (c *volumeContext) Name() string { - return c.v.Name -} - -func (c *volumeContext) Driver() string { - return c.v.Driver -} - -func (c *volumeContext) Scope() string { - return c.v.Scope -} - -func (c *volumeContext) Mountpoint() string { - return c.v.Mountpoint -} - -func (c *volumeContext) Labels() string { - if c.v.Labels == nil { - return "" - } - - var joinLabels []string - for k, v := range c.v.Labels { - joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) - } - return strings.Join(joinLabels, ",") -} - -func (c *volumeContext) Label(name string) string { - if c.v.Labels == nil { - return "" - } - return c.v.Labels[name] -} - -func (c *volumeContext) Links() string { - if c.v.UsageData == nil { - return "N/A" - } - return fmt.Sprintf("%d", c.v.UsageData.RefCount) -} - -func (c *volumeContext) Size() string { - if c.v.UsageData == nil { - return "N/A" - } - return units.HumanSize(float64(c.v.UsageData.Size)) -} diff --git a/cli/command/formatter/volume_test.go b/cli/command/formatter/volume_test.go deleted file mode 100644 index bf1100893f..0000000000 --- a/cli/command/formatter/volume_test.go +++ /dev/null @@ -1,183 +0,0 @@ -package formatter - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" - "github.com/stretchr/testify/assert" -) - -func TestVolumeContext(t *testing.T) { - volumeName := stringid.GenerateRandomID() - - var ctx volumeContext - cases := []struct { - volumeCtx volumeContext - expValue string - call func() string - }{ - {volumeContext{ - v: types.Volume{Name: volumeName}, - }, volumeName, ctx.Name}, - {volumeContext{ - v: types.Volume{Driver: "driver_name"}, - }, "driver_name", ctx.Driver}, - {volumeContext{ - v: types.Volume{Scope: "local"}, - }, "local", ctx.Scope}, - {volumeContext{ - v: types.Volume{Mountpoint: "mountpoint"}, - }, "mountpoint", ctx.Mountpoint}, - {volumeContext{ - v: types.Volume{}, - }, "", ctx.Labels}, - {volumeContext{ - v: types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, - }, "label1=value1,label2=value2", ctx.Labels}, - } - - for _, c := range cases { - ctx = c.volumeCtx - v := c.call() - if strings.Contains(v, ",") { - compareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestVolumeContextWrite(t *testing.T) { - cases := []struct { - context Context - expected string - }{ - - // Errors - { - Context{Format: "{{InvalidFunction}}"}, - `Template parsing error: template: :1: function "InvalidFunction" not defined -`, - }, - { - Context{Format: "{{nil}}"}, - `Template parsing error: template: :1:2: executing "" at : nil is not a command -`, - }, - // Table format - { - Context{Format: NewVolumeFormat("table", false)}, - `DRIVER VOLUME NAME -foo foobar_baz -bar foobar_bar -`, - }, - { - Context{Format: NewVolumeFormat("table", true)}, - `foobar_baz -foobar_bar -`, - }, - { - Context{Format: NewVolumeFormat("table {{.Name}}", false)}, - `VOLUME NAME -foobar_baz -foobar_bar -`, - }, - { - Context{Format: NewVolumeFormat("table {{.Name}}", true)}, - `VOLUME NAME -foobar_baz -foobar_bar -`, - }, - // Raw Format - { - Context{Format: NewVolumeFormat("raw", false)}, - `name: foobar_baz -driver: foo - -name: foobar_bar -driver: bar - -`, - }, - { - Context{Format: NewVolumeFormat("raw", true)}, - `name: foobar_baz -name: foobar_bar -`, - }, - // Custom Format - { - Context{Format: NewVolumeFormat("{{.Name}}", false)}, - `foobar_baz -foobar_bar -`, - }, - } - - for _, testcase := range cases { - volumes := []*types.Volume{ - {Name: "foobar_baz", Driver: "foo"}, - {Name: "foobar_bar", Driver: "bar"}, - } - out := bytes.NewBufferString("") - testcase.context.Output = out - err := VolumeWrite(testcase.context, volumes) - if err != nil { - assert.EqualError(t, err, testcase.expected) - } else { - assert.Equal(t, testcase.expected, out.String()) - } - } -} - -func TestVolumeContextWriteJSON(t *testing.T) { - volumes := []*types.Volume{ - {Driver: "foo", Name: "foobar_baz"}, - {Driver: "bar", Name: "foobar_bar"}, - } - expectedJSONs := []map[string]interface{}{ - {"Driver": "foo", "Labels": "", "Links": "N/A", "Mountpoint": "", "Name": "foobar_baz", "Scope": "", "Size": "N/A"}, - {"Driver": "bar", "Labels": "", "Links": "N/A", "Mountpoint": "", "Name": "foobar_bar", "Scope": "", "Size": "N/A"}, - } - out := bytes.NewBufferString("") - err := VolumeWrite(Context{Format: "{{json .}}", Output: out}, volumes) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var m map[string]interface{} - if err := json.Unmarshal([]byte(line), &m); err != nil { - t.Fatal(err) - } - assert.Equal(t, expectedJSONs[i], m) - } -} - -func TestVolumeContextWriteJSONField(t *testing.T) { - volumes := []*types.Volume{ - {Driver: "foo", Name: "foobar_baz"}, - {Driver: "bar", Name: "foobar_bar"}, - } - out := bytes.NewBufferString("") - err := VolumeWrite(Context{Format: "{{json .Name}}", Output: out}, volumes) - if err != nil { - t.Fatal(err) - } - for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { - t.Logf("Output: line %d: %s", i, line) - var s string - if err := json.Unmarshal([]byte(line), &s); err != nil { - t.Fatal(err) - } - assert.Equal(t, volumes[i].Name, s) - } -} diff --git a/cli/command/idresolver/client_test.go b/cli/command/idresolver/client_test.go deleted file mode 100644 index f84683b907..0000000000 --- a/cli/command/idresolver/client_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package idresolver - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -type fakeClient struct { - client.Client - nodeInspectFunc func(string) (swarm.Node, []byte, error) - serviceInspectFunc func(string) (swarm.Service, []byte, error) -} - -func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error) { - if cli.nodeInspectFunc != nil { - return cli.nodeInspectFunc(nodeID) - } - return swarm.Node{}, []byte{}, nil -} - -func (cli *fakeClient) ServiceInspectWithRaw(ctx context.Context, serviceID string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) { - if cli.serviceInspectFunc != nil { - return cli.serviceInspectFunc(serviceID) - } - return swarm.Service{}, []byte{}, nil -} diff --git a/cli/command/idresolver/idresolver.go b/cli/command/idresolver/idresolver.go deleted file mode 100644 index 6088b64b59..0000000000 --- a/cli/command/idresolver/idresolver.go +++ /dev/null @@ -1,70 +0,0 @@ -package idresolver - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/pkg/errors" -) - -// IDResolver provides ID to Name resolution. -type IDResolver struct { - client client.APIClient - noResolve bool - cache map[string]string -} - -// New creates a new IDResolver. -func New(client client.APIClient, noResolve bool) *IDResolver { - return &IDResolver{ - client: client, - noResolve: noResolve, - cache: make(map[string]string), - } -} - -func (r *IDResolver) get(ctx context.Context, t interface{}, id string) (string, error) { - switch t.(type) { - case swarm.Node: - node, _, err := r.client.NodeInspectWithRaw(ctx, id) - if err != nil { - return id, nil - } - if node.Spec.Annotations.Name != "" { - return node.Spec.Annotations.Name, nil - } - if node.Description.Hostname != "" { - return node.Description.Hostname, nil - } - return id, nil - case swarm.Service: - service, _, err := r.client.ServiceInspectWithRaw(ctx, id, types.ServiceInspectOptions{}) - if err != nil { - return id, nil - } - return service.Spec.Annotations.Name, nil - default: - return "", errors.Errorf("unsupported type") - } - -} - -// Resolve will attempt to resolve an ID to a Name by querying the manager. -// Results are stored into a cache. -// If the `-n` flag is used in the command-line, resolution is disabled. -func (r *IDResolver) Resolve(ctx context.Context, t interface{}, id string) (string, error) { - if r.noResolve { - return id, nil - } - if name, ok := r.cache[id]; ok { - return name, nil - } - name, err := r.get(ctx, t, id) - if err != nil { - return "", err - } - r.cache[id] = name - return name, nil -} diff --git a/cli/command/idresolver/idresolver_test.go b/cli/command/idresolver/idresolver_test.go deleted file mode 100644 index 1aca09ce96..0000000000 --- a/cli/command/idresolver/idresolver_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package idresolver - -import ( - "testing" - - "github.com/docker/docker/api/types/swarm" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "golang.org/x/net/context" -) - -func TestResolveError(t *testing.T) { - cli := &fakeClient{ - nodeInspectFunc: func(nodeID string) (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting node") - }, - } - - idResolver := New(cli, false) - _, err := idResolver.Resolve(context.Background(), struct{}{}, "nodeID") - - assert.EqualError(t, err, "unsupported type") -} - -func TestResolveWithNoResolveOption(t *testing.T) { - resolved := false - cli := &fakeClient{ - nodeInspectFunc: func(nodeID string) (swarm.Node, []byte, error) { - resolved = true - return swarm.Node{}, []byte{}, nil - }, - serviceInspectFunc: func(serviceID string) (swarm.Service, []byte, error) { - resolved = true - return swarm.Service{}, []byte{}, nil - }, - } - - idResolver := New(cli, true) - id, err := idResolver.Resolve(context.Background(), swarm.Node{}, "nodeID") - - assert.NoError(t, err) - assert.Equal(t, "nodeID", id) - assert.False(t, resolved) -} - -func TestResolveWithCache(t *testing.T) { - inspectCounter := 0 - cli := &fakeClient{ - nodeInspectFunc: func(nodeID string) (swarm.Node, []byte, error) { - inspectCounter++ - return *Node(NodeName("node-foo")), []byte{}, nil - }, - } - - idResolver := New(cli, false) - - ctx := context.Background() - for i := 0; i < 2; i++ { - id, err := idResolver.Resolve(ctx, swarm.Node{}, "nodeID") - assert.NoError(t, err) - assert.Equal(t, "node-foo", id) - } - - assert.Equal(t, 1, inspectCounter) -} - -func TestResolveNode(t *testing.T) { - testCases := []struct { - nodeID string - nodeInspectFunc func(string) (swarm.Node, []byte, error) - expectedID string - }{ - { - nodeID: "nodeID", - nodeInspectFunc: func(string) (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting node") - }, - expectedID: "nodeID", - }, - { - nodeID: "nodeID", - nodeInspectFunc: func(string) (swarm.Node, []byte, error) { - return *Node(NodeName("node-foo")), []byte{}, nil - }, - expectedID: "node-foo", - }, - { - nodeID: "nodeID", - nodeInspectFunc: func(string) (swarm.Node, []byte, error) { - return *Node(NodeName(""), Hostname("node-hostname")), []byte{}, nil - }, - expectedID: "node-hostname", - }, - } - - ctx := context.Background() - for _, tc := range testCases { - cli := &fakeClient{ - nodeInspectFunc: tc.nodeInspectFunc, - } - idResolver := New(cli, false) - id, err := idResolver.Resolve(ctx, swarm.Node{}, tc.nodeID) - - assert.NoError(t, err) - assert.Equal(t, tc.expectedID, id) - } -} - -func TestResolveService(t *testing.T) { - testCases := []struct { - serviceID string - serviceInspectFunc func(string) (swarm.Service, []byte, error) - expectedID string - }{ - { - serviceID: "serviceID", - serviceInspectFunc: func(string) (swarm.Service, []byte, error) { - return swarm.Service{}, []byte{}, errors.Errorf("error inspecting service") - }, - expectedID: "serviceID", - }, - { - serviceID: "serviceID", - serviceInspectFunc: func(string) (swarm.Service, []byte, error) { - return *Service(ServiceName("service-foo")), []byte{}, nil - }, - expectedID: "service-foo", - }, - } - - ctx := context.Background() - for _, tc := range testCases { - cli := &fakeClient{ - serviceInspectFunc: tc.serviceInspectFunc, - } - idResolver := New(cli, false) - id, err := idResolver.Resolve(ctx, swarm.Service{}, tc.serviceID) - - assert.NoError(t, err) - assert.Equal(t, tc.expectedID, id) - } -} diff --git a/cli/command/image/build.go b/cli/command/image/build.go deleted file mode 100644 index de12afdad3..0000000000 --- a/cli/command/image/build.go +++ /dev/null @@ -1,558 +0,0 @@ -package image - -import ( - "archive/tar" - "bufio" - "bytes" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "regexp" - "runtime" - "time" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/builder/dockerignore" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/image/build" - "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/archive" - "github.com/docker/docker/pkg/fileutils" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/pkg/progress" - "github.com/docker/docker/pkg/streamformatter" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/urlutil" - runconfigopts "github.com/docker/docker/runconfig/opts" - units "github.com/docker/go-units" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type buildOptions struct { - context string - dockerfileName string - tags opts.ListOpts - labels opts.ListOpts - buildArgs opts.ListOpts - extraHosts opts.ListOpts - ulimits *opts.UlimitOpt - memory opts.MemBytes - memorySwap opts.MemSwapBytes - shmSize opts.MemBytes - cpuShares int64 - cpuPeriod int64 - cpuQuota int64 - cpuSetCpus string - cpuSetMems string - cgroupParent string - isolation string - quiet bool - noCache bool - rm bool - forceRm bool - pull bool - cacheFrom []string - compress bool - securityOpt []string - networkMode string - squash bool - target string - imageIDFile string -} - -// NewBuildCommand creates a new `docker build` command -func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { - ulimits := make(map[string]*units.Ulimit) - options := buildOptions{ - tags: opts.NewListOpts(validateTag), - buildArgs: opts.NewListOpts(opts.ValidateEnv), - ulimits: opts.NewUlimitOpt(&ulimits), - labels: opts.NewListOpts(opts.ValidateEnv), - extraHosts: opts.NewListOpts(opts.ValidateExtraHost), - } - - cmd := &cobra.Command{ - Use: "build [OPTIONS] PATH | URL | -", - Short: "Build an image from a Dockerfile", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - options.context = args[0] - return runBuild(dockerCli, options) - }, - } - - flags := cmd.Flags() - - flags.VarP(&options.tags, "tag", "t", "Name and optionally a tag in the 'name:tag' format") - flags.Var(&options.buildArgs, "build-arg", "Set build-time variables") - flags.Var(options.ulimits, "ulimit", "Ulimit options") - flags.StringVarP(&options.dockerfileName, "file", "f", "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") - flags.VarP(&options.memory, "memory", "m", "Memory limit") - flags.Var(&options.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") - flags.Var(&options.shmSize, "shm-size", "Size of /dev/shm") - flags.Int64VarP(&options.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") - flags.Int64Var(&options.cpuPeriod, "cpu-period", 0, "Limit the CPU CFS (Completely Fair Scheduler) period") - flags.Int64Var(&options.cpuQuota, "cpu-quota", 0, "Limit the CPU CFS (Completely Fair Scheduler) quota") - flags.StringVar(&options.cpuSetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") - flags.StringVar(&options.cpuSetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") - flags.StringVar(&options.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container") - flags.StringVar(&options.isolation, "isolation", "", "Container isolation technology") - flags.Var(&options.labels, "label", "Set metadata for an image") - flags.BoolVar(&options.noCache, "no-cache", false, "Do not use cache when building the image") - flags.BoolVar(&options.rm, "rm", true, "Remove intermediate containers after a successful build") - flags.BoolVar(&options.forceRm, "force-rm", false, "Always remove intermediate containers") - flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the build output and print image ID on success") - flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image") - flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources") - flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip") - flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options") - flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build") - flags.SetAnnotation("network", "version", []string{"1.25"}) - flags.Var(&options.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)") - flags.StringVar(&options.target, "target", "", "Set the target build stage to build.") - flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file") - - command.AddTrustVerificationFlags(flags) - - flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer") - flags.SetAnnotation("squash", "experimental", nil) - flags.SetAnnotation("squash", "version", []string{"1.25"}) - - return cmd -} - -// lastProgressOutput is the same as progress.Output except -// that it only output with the last update. It is used in -// non terminal scenarios to suppress verbose messages -type lastProgressOutput struct { - output progress.Output -} - -// WriteProgress formats progress information from a ProgressReader. -func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error { - if !prog.LastUpdate { - return nil - } - - return out.output.WriteProgress(prog) -} - -func runBuild(dockerCli *command.DockerCli, options buildOptions) error { - var ( - buildCtx io.ReadCloser - dockerfileCtx io.ReadCloser - err error - contextDir string - tempDir string - relDockerfile string - progBuff io.Writer - buildBuff io.Writer - ) - - specifiedContext := options.context - progBuff = dockerCli.Out() - buildBuff = dockerCli.Out() - if options.quiet { - progBuff = bytes.NewBuffer(nil) - buildBuff = bytes.NewBuffer(nil) - } - if options.imageIDFile != "" { - // Avoid leaving a stale file if we eventually fail - if err := os.Remove(options.imageIDFile); err != nil && !os.IsNotExist(err) { - return errors.Wrap(err, "Removing image ID file") - } - } - - if options.dockerfileName == "-" { - if specifiedContext == "-" { - return errors.New("invalid argument: can't use stdin for both build context and dockerfile") - } - dockerfileCtx = dockerCli.In() - } - - switch { - case specifiedContext == "-": - buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName) - case isLocalDir(specifiedContext): - contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName) - case urlutil.IsGitURL(specifiedContext): - tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName) - case urlutil.IsURL(specifiedContext): - buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName) - default: - return errors.Errorf("unable to prepare context: path %q not found", specifiedContext) - } - - if err != nil { - if options.quiet && urlutil.IsURL(specifiedContext) { - fmt.Fprintln(dockerCli.Err(), progBuff) - } - return errors.Errorf("unable to prepare context: %s", err) - } - - if tempDir != "" { - defer os.RemoveAll(tempDir) - contextDir = tempDir - } - - if buildCtx == nil { - // And canonicalize dockerfile name to a platform-independent one - relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) - if err != nil { - return errors.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err) - } - - f, err := os.Open(filepath.Join(contextDir, ".dockerignore")) - if err != nil && !os.IsNotExist(err) { - return err - } - defer f.Close() - - var excludes []string - if err == nil { - excludes, err = dockerignore.ReadAll(f) - if err != nil { - return err - } - } - - if err := build.ValidateContextDirectory(contextDir, excludes); err != nil { - return errors.Errorf("Error checking context: '%s'.", err) - } - - // If .dockerignore mentions .dockerignore or the Dockerfile then make - // sure we send both files over to the daemon because Dockerfile is, - // obviously, needed no matter what, and .dockerignore is needed to know - // if either one needs to be removed. The daemon will remove them - // if necessary, after it parses the Dockerfile. Ignore errors here, as - // they will have been caught by validateContextDirectory above. - // Excludes are used instead of includes to maintain the order of files - // in the archive. - if keep, _ := fileutils.Matches(".dockerignore", excludes); keep { - excludes = append(excludes, "!.dockerignore") - } - if keep, _ := fileutils.Matches(relDockerfile, excludes); keep && dockerfileCtx == nil { - excludes = append(excludes, "!"+relDockerfile) - } - - compression := archive.Uncompressed - if options.compress { - compression = archive.Gzip - } - buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ - Compression: compression, - ExcludePatterns: excludes, - }) - if err != nil { - return err - } - } - - // replace Dockerfile if added dynamically - if dockerfileCtx != nil { - buildCtx, relDockerfile, err = addDockerfileToBuildContext(dockerfileCtx, buildCtx) - if err != nil { - return err - } - } - - ctx := context.Background() - - var resolvedTags []*resolvedTag - if command.IsTrusted() { - translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) { - return TrustedReference(ctx, dockerCli, ref, nil) - } - // Wrap the tar archive to replace the Dockerfile entry with the rewritten - // Dockerfile which uses trusted pulls. - buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, translator, &resolvedTags) - } - - // Setup an upload progress bar - progressOutput := streamformatter.NewProgressOutput(progBuff) - if !dockerCli.Out().IsTerminal() { - progressOutput = &lastProgressOutput{output: progressOutput} - } - - var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon") - - authConfigs, _ := dockerCli.GetAllCredentials() - buildOptions := types.ImageBuildOptions{ - Memory: options.memory.Value(), - MemorySwap: options.memorySwap.Value(), - Tags: options.tags.GetAll(), - SuppressOutput: options.quiet, - NoCache: options.noCache, - Remove: options.rm, - ForceRemove: options.forceRm, - PullParent: options.pull, - Isolation: container.Isolation(options.isolation), - CPUSetCPUs: options.cpuSetCpus, - CPUSetMems: options.cpuSetMems, - CPUShares: options.cpuShares, - CPUQuota: options.cpuQuota, - CPUPeriod: options.cpuPeriod, - CgroupParent: options.cgroupParent, - Dockerfile: relDockerfile, - ShmSize: options.shmSize.Value(), - Ulimits: options.ulimits.GetList(), - BuildArgs: runconfigopts.ConvertKVStringsToMapWithNil(options.buildArgs.GetAll()), - AuthConfigs: authConfigs, - Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), - CacheFrom: options.cacheFrom, - SecurityOpt: options.securityOpt, - NetworkMode: options.networkMode, - Squash: options.squash, - ExtraHosts: options.extraHosts.GetAll(), - Target: options.target, - } - - response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) - if err != nil { - if options.quiet { - fmt.Fprintf(dockerCli.Err(), "%s", progBuff) - } - return err - } - defer response.Body.Close() - - imageID := "" - aux := func(auxJSON *json.RawMessage) { - var result types.BuildResult - if err := json.Unmarshal(*auxJSON, &result); err != nil { - fmt.Fprintf(dockerCli.Err(), "Failed to parse aux message: %s", err) - } else { - imageID = result.ID - } - } - - err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), aux) - if err != nil { - if jerr, ok := err.(*jsonmessage.JSONError); ok { - // If no error code is set, default to 1 - if jerr.Code == 0 { - jerr.Code = 1 - } - if options.quiet { - fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff) - } - return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} - } - return err - } - - // Windows: show error message about modified file permissions if the - // daemon isn't running Windows. - if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet { - fmt.Fprintln(dockerCli.Out(), "SECURITY WARNING: You are building a Docker "+ - "image from Windows against a non-Windows Docker host. All files and "+ - "directories added to build context will have '-rwxr-xr-x' permissions. "+ - "It is recommended to double check and reset permissions for sensitive "+ - "files and directories.") - } - - // Everything worked so if -q was provided the output from the daemon - // should be just the image ID and we'll print that to stdout. - if options.quiet { - imageID = fmt.Sprintf("%s", buildBuff) - fmt.Fprintf(dockerCli.Out(), imageID) - } - - if options.imageIDFile != "" { - if imageID == "" { - return errors.Errorf("Server did not provide an image ID. Cannot write %s", options.imageIDFile) - } - if err := ioutil.WriteFile(options.imageIDFile, []byte(imageID), 0666); err != nil { - return err - } - } - if command.IsTrusted() { - // Since the build was successful, now we must tag any of the resolved - // images from the above Dockerfile rewrite. - for _, resolved := range resolvedTags { - if err := TagTrusted(ctx, dockerCli, resolved.digestRef, resolved.tagRef); err != nil { - return err - } - } - } - - return nil -} - -func addDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCloser) (io.ReadCloser, string, error) { - file, err := ioutil.ReadAll(dockerfileCtx) - dockerfileCtx.Close() - if err != nil { - return nil, "", err - } - now := time.Now() - hdrTmpl := &tar.Header{ - Mode: 0600, - Uid: 0, - Gid: 0, - ModTime: now, - Typeflag: tar.TypeReg, - AccessTime: now, - ChangeTime: now, - } - randomName := ".dockerfile." + stringid.GenerateRandomID()[:20] - - buildCtx = archive.ReplaceFileTarWrapper(buildCtx, map[string]archive.TarModifierFunc{ - // Add the dockerfile with a random filename - randomName: func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) { - return hdrTmpl, file, nil - }, - // Update .dockerignore to include the random filename - ".dockerignore": func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) { - if h == nil { - h = hdrTmpl - } - - b := &bytes.Buffer{} - if content != nil { - if _, err := b.ReadFrom(content); err != nil { - return nil, nil, err - } - } else { - b.WriteString(".dockerignore") - } - b.WriteString("\n" + randomName + "\n") - return h, b.Bytes(), nil - }, - }) - return buildCtx, randomName, nil -} - -func isLocalDir(c string) bool { - _, err := os.Stat(c) - return err == nil -} - -type translatorFunc func(context.Context, reference.NamedTagged) (reference.Canonical, error) - -// validateTag checks if the given image name can be resolved. -func validateTag(rawRepo string) (string, error) { - _, err := reference.ParseNormalizedNamed(rawRepo) - if err != nil { - return "", err - } - - return rawRepo, nil -} - -var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P[^ \f\r\t\v\n#]+)`) - -// resolvedTag records the repository, tag, and resolved digest reference -// from a Dockerfile rewrite. -type resolvedTag struct { - digestRef reference.Canonical - tagRef reference.NamedTagged -} - -// rewriteDockerfileFrom rewrites the given Dockerfile by resolving images in -// "FROM " instructions to a digest reference. `translator` is a -// function that takes a repository name and tag reference and returns a -// trusted digest reference. -func rewriteDockerfileFrom(ctx context.Context, dockerfile io.Reader, translator translatorFunc) (newDockerfile []byte, resolvedTags []*resolvedTag, err error) { - scanner := bufio.NewScanner(dockerfile) - buf := bytes.NewBuffer(nil) - - // Scan the lines of the Dockerfile, looking for a "FROM" line. - for scanner.Scan() { - line := scanner.Text() - - matches := dockerfileFromLinePattern.FindStringSubmatch(line) - if matches != nil && matches[1] != api.NoBaseImageSpecifier { - // Replace the line with a resolved "FROM repo@digest" - var ref reference.Named - ref, err = reference.ParseNormalizedNamed(matches[1]) - if err != nil { - return nil, nil, err - } - ref = reference.TagNameOnly(ref) - if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() { - trustedRef, err := translator(ctx, ref) - if err != nil { - return nil, nil, err - } - - line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", reference.FamiliarString(trustedRef))) - resolvedTags = append(resolvedTags, &resolvedTag{ - digestRef: trustedRef, - tagRef: ref, - }) - } - } - - _, err := fmt.Fprintln(buf, line) - if err != nil { - return nil, nil, err - } - } - - return buf.Bytes(), resolvedTags, scanner.Err() -} - -// replaceDockerfileTarWrapper wraps the given input tar archive stream and -// replaces the entry with the given Dockerfile name with the contents of the -// new Dockerfile. Returns a new tar archive stream with the replaced -// Dockerfile. -func replaceDockerfileTarWrapper(ctx context.Context, inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser { - pipeReader, pipeWriter := io.Pipe() - go func() { - tarReader := tar.NewReader(inputTarStream) - tarWriter := tar.NewWriter(pipeWriter) - - defer inputTarStream.Close() - - for { - hdr, err := tarReader.Next() - if err == io.EOF { - // Signals end of archive. - tarWriter.Close() - pipeWriter.Close() - return - } - if err != nil { - pipeWriter.CloseWithError(err) - return - } - - content := io.Reader(tarReader) - if hdr.Name == dockerfileName { - // This entry is the Dockerfile. Since the tar archive was - // generated from a directory on the local filesystem, the - // Dockerfile will only appear once in the archive. - var newDockerfile []byte - newDockerfile, *resolvedTags, err = rewriteDockerfileFrom(ctx, content, translator) - if err != nil { - pipeWriter.CloseWithError(err) - return - } - hdr.Size = int64(len(newDockerfile)) - content = bytes.NewBuffer(newDockerfile) - } - - if err := tarWriter.WriteHeader(hdr); err != nil { - pipeWriter.CloseWithError(err) - return - } - - if _, err := io.Copy(tarWriter, content); err != nil { - pipeWriter.CloseWithError(err) - return - } - } - }() - - return pipeReader -} diff --git a/cli/command/image/build/context.go b/cli/command/image/build/context.go deleted file mode 100644 index 5e66717a04..0000000000 --- a/cli/command/image/build/context.go +++ /dev/null @@ -1,275 +0,0 @@ -package build - -import ( - "bufio" - "fmt" - "io" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - - "github.com/docker/docker/pkg/archive" - "github.com/docker/docker/pkg/fileutils" - "github.com/docker/docker/pkg/gitutils" - "github.com/docker/docker/pkg/httputils" - "github.com/docker/docker/pkg/ioutils" - "github.com/docker/docker/pkg/progress" - "github.com/docker/docker/pkg/streamformatter" - "github.com/pkg/errors" -) - -const ( - // DefaultDockerfileName is the Default filename with Docker commands, read by docker build - DefaultDockerfileName string = "Dockerfile" -) - -// ValidateContextDirectory checks if all the contents of the directory -// can be read and returns an error if some files can't be read -// symlinks which point to non-existing files don't trigger an error -func ValidateContextDirectory(srcPath string, excludes []string) error { - contextRoot, err := getContextRoot(srcPath) - if err != nil { - return err - } - return filepath.Walk(contextRoot, func(filePath string, f os.FileInfo, err error) error { - if err != nil { - if os.IsPermission(err) { - return errors.Errorf("can't stat '%s'", filePath) - } - if os.IsNotExist(err) { - return nil - } - return err - } - - // skip this directory/file if it's not in the path, it won't get added to the context - if relFilePath, err := filepath.Rel(contextRoot, filePath); err != nil { - return err - } else if skip, err := fileutils.Matches(relFilePath, excludes); err != nil { - return err - } else if skip { - if f.IsDir() { - return filepath.SkipDir - } - return nil - } - - // skip checking if symlinks point to non-existing files, such symlinks can be useful - // also skip named pipes, because they hanging on open - if f.Mode()&(os.ModeSymlink|os.ModeNamedPipe) != 0 { - return nil - } - - if !f.IsDir() { - currentFile, err := os.Open(filePath) - if err != nil && os.IsPermission(err) { - return errors.Errorf("no permission to read from '%s'", filePath) - } - currentFile.Close() - } - return nil - }) -} - -// GetContextFromReader will read the contents of the given reader as either a -// Dockerfile or tar archive. Returns a tar archive used as a context and a -// path to the Dockerfile inside the tar. -func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) { - buf := bufio.NewReader(r) - - magic, err := buf.Peek(archive.HeaderSize) - if err != nil && err != io.EOF { - return nil, "", errors.Errorf("failed to peek context header from STDIN: %v", err) - } - - if archive.IsArchive(magic) { - return ioutils.NewReadCloserWrapper(buf, func() error { return r.Close() }), dockerfileName, nil - } - - if dockerfileName == "-" { - return nil, "", errors.New("build context is not an archive") - } - - // Input should be read as a Dockerfile. - tmpDir, err := ioutil.TempDir("", "docker-build-context-") - if err != nil { - return nil, "", errors.Errorf("unable to create temporary context directory: %v", err) - } - - f, err := os.Create(filepath.Join(tmpDir, DefaultDockerfileName)) - if err != nil { - return nil, "", err - } - _, err = io.Copy(f, buf) - if err != nil { - f.Close() - return nil, "", err - } - - if err := f.Close(); err != nil { - return nil, "", err - } - if err := r.Close(); err != nil { - return nil, "", err - } - - tar, err := archive.Tar(tmpDir, archive.Uncompressed) - if err != nil { - return nil, "", err - } - - return ioutils.NewReadCloserWrapper(tar, func() error { - err := tar.Close() - os.RemoveAll(tmpDir) - return err - }), DefaultDockerfileName, nil - -} - -// GetContextFromGitURL uses a Git URL as context for a `docker build`. The -// git repo is cloned into a temporary directory used as the context directory. -// Returns the absolute path to the temporary context directory, the relative -// path of the dockerfile in that context directory, and a non-nil error on -// success. -func GetContextFromGitURL(gitURL, dockerfileName string) (absContextDir, relDockerfile string, err error) { - if _, err := exec.LookPath("git"); err != nil { - return "", "", errors.Errorf("unable to find 'git': %v", err) - } - if absContextDir, err = gitutils.Clone(gitURL); err != nil { - return "", "", errors.Errorf("unable to 'git clone' to temporary context directory: %v", err) - } - - return getDockerfileRelPath(absContextDir, dockerfileName) -} - -// GetContextFromURL uses a remote URL as context for a `docker build`. The -// remote resource is downloaded as either a Dockerfile or a tar archive. -// Returns the tar archive used for the context and a path of the -// dockerfile inside the tar. -func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.ReadCloser, string, error) { - response, err := httputils.Download(remoteURL) - if err != nil { - return nil, "", errors.Errorf("unable to download remote context %s: %v", remoteURL, err) - } - progressOutput := streamformatter.NewProgressOutput(out) - - // Pass the response body through a progress reader. - progReader := progress.NewProgressReader(response.Body, progressOutput, response.ContentLength, "", fmt.Sprintf("Downloading build context from remote url: %s", remoteURL)) - - return GetContextFromReader(ioutils.NewReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName) -} - -// GetContextFromLocalDir uses the given local directory as context for a -// `docker build`. Returns the absolute path to the local context directory, -// the relative path of the dockerfile in that context directory, and a non-nil -// error on success. -func GetContextFromLocalDir(localDir, dockerfileName string) (absContextDir, relDockerfile string, err error) { - // When using a local context directory, when the Dockerfile is specified - // with the `-f/--file` option then it is considered relative to the - // current directory and not the context directory. - if dockerfileName != "" && dockerfileName != "-" { - if dockerfileName, err = filepath.Abs(dockerfileName); err != nil { - return "", "", errors.Errorf("unable to get absolute path to Dockerfile: %v", err) - } - } - - return getDockerfileRelPath(localDir, dockerfileName) -} - -// getDockerfileRelPath uses the given context directory for a `docker build` -// and returns the absolute path to the context directory, the relative path of -// the dockerfile in that context directory, and a non-nil error on success. -func getDockerfileRelPath(givenContextDir, givenDockerfile string) (absContextDir, relDockerfile string, err error) { - if absContextDir, err = filepath.Abs(givenContextDir); err != nil { - return "", "", errors.Errorf("unable to get absolute context directory of given context directory %q: %v", givenContextDir, err) - } - - // The context dir might be a symbolic link, so follow it to the actual - // target directory. - // - // FIXME. We use isUNC (always false on non-Windows platforms) to workaround - // an issue in golang. On Windows, EvalSymLinks does not work on UNC file - // paths (those starting with \\). This hack means that when using links - // on UNC paths, they will not be followed. - if !isUNC(absContextDir) { - absContextDir, err = filepath.EvalSymlinks(absContextDir) - if err != nil { - return "", "", errors.Errorf("unable to evaluate symlinks in context path: %v", err) - } - } - - stat, err := os.Lstat(absContextDir) - if err != nil { - return "", "", errors.Errorf("unable to stat context directory %q: %v", absContextDir, err) - } - - if !stat.IsDir() { - return "", "", errors.Errorf("context must be a directory: %s", absContextDir) - } - - absDockerfile := givenDockerfile - if absDockerfile == "" { - // No -f/--file was specified so use the default relative to the - // context directory. - absDockerfile = filepath.Join(absContextDir, DefaultDockerfileName) - - // Just to be nice ;-) look for 'dockerfile' too but only - // use it if we found it, otherwise ignore this check - if _, err = os.Lstat(absDockerfile); os.IsNotExist(err) { - altPath := filepath.Join(absContextDir, strings.ToLower(DefaultDockerfileName)) - if _, err = os.Lstat(altPath); err == nil { - absDockerfile = altPath - } - } - } else if absDockerfile == "-" { - absDockerfile = filepath.Join(absContextDir, DefaultDockerfileName) - } - - // If not already an absolute path, the Dockerfile path should be joined to - // the base directory. - if !filepath.IsAbs(absDockerfile) { - absDockerfile = filepath.Join(absContextDir, absDockerfile) - } - - // Evaluate symlinks in the path to the Dockerfile too. - // - // FIXME. We use isUNC (always false on non-Windows platforms) to workaround - // an issue in golang. On Windows, EvalSymLinks does not work on UNC file - // paths (those starting with \\). This hack means that when using links - // on UNC paths, they will not be followed. - if givenDockerfile != "-" { - if !isUNC(absDockerfile) { - absDockerfile, err = filepath.EvalSymlinks(absDockerfile) - if err != nil { - return "", "", errors.Errorf("unable to evaluate symlinks in Dockerfile path: %v", err) - - } - } - - if _, err := os.Lstat(absDockerfile); err != nil { - if os.IsNotExist(err) { - return "", "", errors.Errorf("Cannot locate Dockerfile: %q", absDockerfile) - } - return "", "", errors.Errorf("unable to stat Dockerfile: %v", err) - } - } - - if relDockerfile, err = filepath.Rel(absContextDir, absDockerfile); err != nil { - return "", "", errors.Errorf("unable to get relative Dockerfile path: %v", err) - } - - if strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { - return "", "", errors.Errorf("The Dockerfile (%s) must be within the build context (%s)", givenDockerfile, givenContextDir) - } - - return absContextDir, relDockerfile, nil -} - -// isUNC returns true if the path is UNC (one starting \\). It always returns -// false on Linux. -func isUNC(path string) bool { - return runtime.GOOS == "windows" && strings.HasPrefix(path, `\\`) -} diff --git a/cli/command/image/build/context_test.go b/cli/command/image/build/context_test.go deleted file mode 100644 index afa04a4fcd..0000000000 --- a/cli/command/image/build/context_test.go +++ /dev/null @@ -1,383 +0,0 @@ -package build - -import ( - "archive/tar" - "bytes" - "io" - "io/ioutil" - "os" - "path/filepath" - "runtime" - "strings" - "testing" - - "github.com/docker/docker/pkg/archive" -) - -const dockerfileContents = "FROM busybox" - -var prepareEmpty = func(t *testing.T) (string, func()) { - return "", func() {} -} - -var prepareNoFiles = func(t *testing.T) (string, func()) { - return createTestTempDir(t, "", "builder-context-test") -} - -var prepareOneFile = func(t *testing.T) (string, func()) { - contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) - return contextDir, cleanup -} - -func testValidateContextDirectory(t *testing.T, prepare func(t *testing.T) (string, func()), excludes []string) { - contextDir, cleanup := prepare(t) - defer cleanup() - - err := ValidateContextDirectory(contextDir, excludes) - - if err != nil { - t.Fatalf("Error should be nil, got: %s", err) - } -} - -func TestGetContextFromLocalDirNoDockerfile(t *testing.T) { - contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") - defer cleanup() - - absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "") - - if err == nil { - t.Fatalf("Error should not be nil") - } - - if absContextDir != "" { - t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir) - } - - if relDockerfile != "" { - t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile) - } -} - -func TestGetContextFromLocalDirNotExistingDir(t *testing.T) { - contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") - defer cleanup() - - fakePath := filepath.Join(contextDir, "fake") - - absContextDir, relDockerfile, err := GetContextFromLocalDir(fakePath, "") - - if err == nil { - t.Fatalf("Error should not be nil") - } - - if absContextDir != "" { - t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir) - } - - if relDockerfile != "" { - t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile) - } -} - -func TestGetContextFromLocalDirNotExistingDockerfile(t *testing.T) { - contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") - defer cleanup() - - fakePath := filepath.Join(contextDir, "fake") - - absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, fakePath) - - if err == nil { - t.Fatalf("Error should not be nil") - } - - if absContextDir != "" { - t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir) - } - - if relDockerfile != "" { - t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile) - } -} - -func TestGetContextFromLocalDirWithNoDirectory(t *testing.T) { - contextDir, dirCleanup := createTestTempDir(t, "", "builder-context-test") - defer dirCleanup() - - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) - - chdirCleanup := chdir(t, contextDir) - defer chdirCleanup() - - absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "") - - if err != nil { - t.Fatalf("Error when getting context from local dir: %s", err) - } - - if absContextDir != contextDir { - t.Fatalf("Absolute directory path should be equal to %s, got: %s", contextDir, absContextDir) - } - - if relDockerfile != DefaultDockerfileName { - t.Fatalf("Relative path to dockerfile should be equal to %s, got: %s", DefaultDockerfileName, relDockerfile) - } -} - -func TestGetContextFromLocalDirWithDockerfile(t *testing.T) { - contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") - defer cleanup() - - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) - - absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "") - - if err != nil { - t.Fatalf("Error when getting context from local dir: %s", err) - } - - if absContextDir != contextDir { - t.Fatalf("Absolute directory path should be equal to %s, got: %s", contextDir, absContextDir) - } - - if relDockerfile != DefaultDockerfileName { - t.Fatalf("Relative path to dockerfile should be equal to %s, got: %s", DefaultDockerfileName, relDockerfile) - } -} - -func TestGetContextFromLocalDirLocalFile(t *testing.T) { - contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") - defer cleanup() - - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) - testFilename := createTestTempFile(t, contextDir, "tmpTest", "test", 0777) - - absContextDir, relDockerfile, err := GetContextFromLocalDir(testFilename, "") - - if err == nil { - t.Fatalf("Error should not be nil") - } - - if absContextDir != "" { - t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir) - } - - if relDockerfile != "" { - t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile) - } -} - -func TestGetContextFromLocalDirWithCustomDockerfile(t *testing.T) { - contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") - defer cleanup() - - chdirCleanup := chdir(t, contextDir) - defer chdirCleanup() - - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) - - absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, DefaultDockerfileName) - - if err != nil { - t.Fatalf("Error when getting context from local dir: %s", err) - } - - if absContextDir != contextDir { - t.Fatalf("Absolute directory path should be equal to %s, got: %s", contextDir, absContextDir) - } - - if relDockerfile != DefaultDockerfileName { - t.Fatalf("Relative path to dockerfile should be equal to %s, got: %s", DefaultDockerfileName, relDockerfile) - } - -} - -func TestGetContextFromReaderString(t *testing.T) { - tarArchive, relDockerfile, err := GetContextFromReader(ioutil.NopCloser(strings.NewReader(dockerfileContents)), "") - - if err != nil { - t.Fatalf("Error when executing GetContextFromReader: %s", err) - } - - tarReader := tar.NewReader(tarArchive) - - _, err = tarReader.Next() - - if err != nil { - t.Fatalf("Error when reading tar archive: %s", err) - } - - buff := new(bytes.Buffer) - buff.ReadFrom(tarReader) - contents := buff.String() - - _, err = tarReader.Next() - - if err != io.EOF { - t.Fatalf("Tar stream too long: %s", err) - } - - if err = tarArchive.Close(); err != nil { - t.Fatalf("Error when closing tar stream: %s", err) - } - - if dockerfileContents != contents { - t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContents, contents) - } - - if relDockerfile != DefaultDockerfileName { - t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile) - } -} - -func TestGetContextFromReaderTar(t *testing.T) { - contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") - defer cleanup() - - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) - - tarStream, err := archive.Tar(contextDir, archive.Uncompressed) - - if err != nil { - t.Fatalf("Error when creating tar: %s", err) - } - - tarArchive, relDockerfile, err := GetContextFromReader(tarStream, DefaultDockerfileName) - - if err != nil { - t.Fatalf("Error when executing GetContextFromReader: %s", err) - } - - tarReader := tar.NewReader(tarArchive) - - header, err := tarReader.Next() - - if err != nil { - t.Fatalf("Error when reading tar archive: %s", err) - } - - if header.Name != DefaultDockerfileName { - t.Fatalf("Dockerfile name should be: %s, got: %s", DefaultDockerfileName, header.Name) - } - - buff := new(bytes.Buffer) - buff.ReadFrom(tarReader) - contents := buff.String() - - _, err = tarReader.Next() - - if err != io.EOF { - t.Fatalf("Tar stream too long: %s", err) - } - - if err = tarArchive.Close(); err != nil { - t.Fatalf("Error when closing tar stream: %s", err) - } - - if dockerfileContents != contents { - t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContents, contents) - } - - if relDockerfile != DefaultDockerfileName { - t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile) - } -} - -func TestValidateContextDirectoryEmptyContext(t *testing.T) { - // This isn't a valid test on Windows. See https://play.golang.org/p/RR6z6jxR81. - // The test will ultimately end up calling filepath.Abs(""). On Windows, - // golang will error. On Linux, golang will return /. Due to there being - // drive letters on Windows, this is probably the correct behaviour for - // Windows. - if runtime.GOOS == "windows" { - t.Skip("Invalid test on Windows") - } - testValidateContextDirectory(t, prepareEmpty, []string{}) -} - -func TestValidateContextDirectoryContextWithNoFiles(t *testing.T) { - testValidateContextDirectory(t, prepareNoFiles, []string{}) -} - -func TestValidateContextDirectoryWithOneFile(t *testing.T) { - testValidateContextDirectory(t, prepareOneFile, []string{}) -} - -func TestValidateContextDirectoryWithOneFileExcludes(t *testing.T) { - testValidateContextDirectory(t, prepareOneFile, []string{DefaultDockerfileName}) -} - -// createTestTempDir creates a temporary directory for testing. -// It returns the created path and a cleanup function which is meant to be used as deferred call. -// When an error occurs, it terminates the test. -func createTestTempDir(t *testing.T, dir, prefix string) (string, func()) { - path, err := ioutil.TempDir(dir, prefix) - - if err != nil { - t.Fatalf("Error when creating directory %s with prefix %s: %s", dir, prefix, err) - } - - return path, func() { - err = os.RemoveAll(path) - - if err != nil { - t.Fatalf("Error when removing directory %s: %s", path, err) - } - } -} - -// createTestTempSubdir creates a temporary directory for testing. -// It returns the created path but doesn't provide a cleanup function, -// so createTestTempSubdir should be used only for creating temporary subdirectories -// whose parent directories are properly cleaned up. -// When an error occurs, it terminates the test. -func createTestTempSubdir(t *testing.T, dir, prefix string) string { - path, err := ioutil.TempDir(dir, prefix) - - if err != nil { - t.Fatalf("Error when creating directory %s with prefix %s: %s", dir, prefix, err) - } - - return path -} - -// createTestTempFile creates a temporary file within dir with specific contents and permissions. -// When an error occurs, it terminates the test -func createTestTempFile(t *testing.T, dir, filename, contents string, perm os.FileMode) string { - filePath := filepath.Join(dir, filename) - err := ioutil.WriteFile(filePath, []byte(contents), perm) - - if err != nil { - t.Fatalf("Error when creating %s file: %s", filename, err) - } - - return filePath -} - -// chdir changes current working directory to dir. -// It returns a function which changes working directory back to the previous one. -// This function is meant to be executed as a deferred call. -// When an error occurs, it terminates the test. -func chdir(t *testing.T, dir string) func() { - workingDirectory, err := os.Getwd() - - if err != nil { - t.Fatalf("Error when retrieving working directory: %s", err) - } - - err = os.Chdir(dir) - - if err != nil { - t.Fatalf("Error when changing directory to %s: %s", dir, err) - } - - return func() { - err = os.Chdir(workingDirectory) - - if err != nil { - t.Fatalf("Error when changing back to working directory (%s): %s", workingDirectory, err) - } - } -} diff --git a/cli/command/image/build/context_unix.go b/cli/command/image/build/context_unix.go deleted file mode 100644 index cb2634f079..0000000000 --- a/cli/command/image/build/context_unix.go +++ /dev/null @@ -1,11 +0,0 @@ -// +build !windows - -package build - -import ( - "path/filepath" -) - -func getContextRoot(srcPath string) (string, error) { - return filepath.Join(srcPath, "."), nil -} diff --git a/cli/command/image/build/context_windows.go b/cli/command/image/build/context_windows.go deleted file mode 100644 index c577cfa7be..0000000000 --- a/cli/command/image/build/context_windows.go +++ /dev/null @@ -1,17 +0,0 @@ -// +build windows - -package build - -import ( - "path/filepath" - - "github.com/docker/docker/pkg/longpath" -) - -func getContextRoot(srcPath string) (string, error) { - cr, err := filepath.Abs(srcPath) - if err != nil { - return "", err - } - return longpath.AddPrefix(cr), nil -} diff --git a/cli/command/image/client_test.go b/cli/command/image/client_test.go deleted file mode 100644 index 0df6fa4f77..0000000000 --- a/cli/command/image/client_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package image - -import ( - "io" - "io/ioutil" - "strings" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -type fakeClient struct { - client.Client - imageTagFunc func(string, string) error - imageSaveFunc func(images []string) (io.ReadCloser, error) - imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) - imagePushFunc func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) - infoFunc func() (types.Info, error) - imagePullFunc func(ref string, options types.ImagePullOptions) (io.ReadCloser, error) - imagesPruneFunc func(pruneFilter filters.Args) (types.ImagesPruneReport, error) - imageLoadFunc func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) - imageListFunc func(options types.ImageListOptions) ([]types.ImageSummary, error) - imageInspectFunc func(image string) (types.ImageInspect, []byte, error) - imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) - imageHistoryFunc func(image string) ([]image.HistoryResponseItem, error) -} - -func (cli *fakeClient) ImageTag(_ context.Context, image, ref string) error { - if cli.imageTagFunc != nil { - return cli.imageTagFunc(image, ref) - } - return nil -} - -func (cli *fakeClient) ImageSave(_ context.Context, images []string) (io.ReadCloser, error) { - if cli.imageSaveFunc != nil { - return cli.imageSaveFunc(images) - } - return ioutil.NopCloser(strings.NewReader("")), nil -} - -func (cli *fakeClient) ImageRemove(_ context.Context, image string, - options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { - if cli.imageRemoveFunc != nil { - return cli.imageRemoveFunc(image, options) - } - return []types.ImageDeleteResponseItem{}, nil -} - -func (cli *fakeClient) ImagePush(_ context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) { - if cli.imagePushFunc != nil { - return cli.imagePushFunc(ref, options) - } - return ioutil.NopCloser(strings.NewReader("")), nil -} - -func (cli *fakeClient) Info(_ context.Context) (types.Info, error) { - if cli.infoFunc != nil { - return cli.infoFunc() - } - return types.Info{}, nil -} - -func (cli *fakeClient) ImagePull(_ context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) { - if cli.imagePullFunc != nil { - cli.imagePullFunc(ref, options) - } - return ioutil.NopCloser(strings.NewReader("")), nil -} - -func (cli *fakeClient) ImagesPrune(_ context.Context, pruneFilter filters.Args) (types.ImagesPruneReport, error) { - if cli.imagesPruneFunc != nil { - return cli.imagesPruneFunc(pruneFilter) - } - return types.ImagesPruneReport{}, nil -} - -func (cli *fakeClient) ImageLoad(_ context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) { - if cli.imageLoadFunc != nil { - return cli.imageLoadFunc(input, quiet) - } - return types.ImageLoadResponse{}, nil -} - -func (cli *fakeClient) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error) { - if cli.imageListFunc != nil { - return cli.imageListFunc(options) - } - return []types.ImageSummary{{}}, nil -} - -func (cli *fakeClient) ImageInspectWithRaw(_ context.Context, image string) (types.ImageInspect, []byte, error) { - if cli.imageInspectFunc != nil { - return cli.imageInspectFunc(image) - } - return types.ImageInspect{}, nil, nil -} - -func (cli *fakeClient) ImageImport(_ context.Context, source types.ImageImportSource, ref string, - options types.ImageImportOptions) (io.ReadCloser, error) { - if cli.imageImportFunc != nil { - return cli.imageImportFunc(source, ref, options) - } - return ioutil.NopCloser(strings.NewReader("")), nil -} - -func (cli *fakeClient) ImageHistory(_ context.Context, img string) ([]image.HistoryResponseItem, error) { - if cli.imageHistoryFunc != nil { - return cli.imageHistoryFunc(img) - } - return []image.HistoryResponseItem{{ID: img, Created: time.Now().Unix()}}, nil -} diff --git a/cli/command/image/cmd.go b/cli/command/image/cmd.go deleted file mode 100644 index c3ca61f85b..0000000000 --- a/cli/command/image/cmd.go +++ /dev/null @@ -1,33 +0,0 @@ -package image - -import ( - "github.com/spf13/cobra" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" -) - -// NewImageCommand returns a cobra command for `image` subcommands -func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "image", - Short: "Manage images", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - } - cmd.AddCommand( - NewBuildCommand(dockerCli), - NewHistoryCommand(dockerCli), - NewImportCommand(dockerCli), - NewLoadCommand(dockerCli), - NewPullCommand(dockerCli), - NewPushCommand(dockerCli), - NewSaveCommand(dockerCli), - NewTagCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - newInspectCommand(dockerCli), - NewPruneCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/image/history.go b/cli/command/image/history.go deleted file mode 100644 index 3e98c228bd..0000000000 --- a/cli/command/image/history.go +++ /dev/null @@ -1,64 +0,0 @@ -package image - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/spf13/cobra" -) - -type historyOptions struct { - image string - - human bool - quiet bool - noTrunc bool - format string -} - -// NewHistoryCommand creates a new `docker history` command -func NewHistoryCommand(dockerCli command.Cli) *cobra.Command { - var opts historyOptions - - cmd := &cobra.Command{ - Use: "history [OPTIONS] IMAGE", - Short: "Show the history of an image", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.image = args[0] - return runHistory(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.BoolVarP(&opts.human, "human", "H", true, "Print sizes and dates in human readable format") - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show numeric IDs") - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") - flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template") - - return cmd -} - -func runHistory(dockerCli command.Cli, opts historyOptions) error { - ctx := context.Background() - - history, err := dockerCli.Client().ImageHistory(ctx, opts.image) - if err != nil { - return err - } - - format := opts.format - if len(format) == 0 { - format = formatter.TableFormatKey - } - - historyCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewHistoryFormat(format, opts.quiet, opts.human), - Trunc: !opts.noTrunc, - } - return formatter.HistoryWrite(historyCtx, opts.human, history) -} diff --git a/cli/command/image/history_test.go b/cli/command/image/history_test.go deleted file mode 100644 index ec0a64f207..0000000000 --- a/cli/command/image/history_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package image - -import ( - "bytes" - "fmt" - "io/ioutil" - "regexp" - "testing" - "time" - - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestNewHistoryCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - imageHistoryFunc func(img string) ([]image.HistoryResponseItem, error) - }{ - { - name: "wrong-args", - args: []string{}, - expectedError: "requires exactly 1 argument(s).", - }, - { - name: "client-error", - args: []string{"image:tag"}, - expectedError: "something went wrong", - imageHistoryFunc: func(img string) ([]image.HistoryResponseItem, error) { - return []image.HistoryResponseItem{{}}, errors.Errorf("something went wrong") - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewHistoryCommandSuccess(t *testing.T) { - testCases := []struct { - name string - args []string - outputRegex string - imageHistoryFunc func(img string) ([]image.HistoryResponseItem, error) - }{ - { - name: "simple", - args: []string{"image:tag"}, - imageHistoryFunc: func(img string) ([]image.HistoryResponseItem, error) { - return []image.HistoryResponseItem{{ - ID: "1234567890123456789", - Created: time.Now().Unix(), - }}, nil - }, - }, - { - name: "quiet", - args: []string{"--quiet", "image:tag"}, - }, - // TODO: This test is failing since the output does not contain an RFC3339 date - //{ - // name: "non-human", - // args: []string{"--human=false", "image:tag"}, - // outputRegex: "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", // RFC3339 date format match - //}, - { - name: "non-human-header", - args: []string{"--human=false", "image:tag"}, - outputRegex: "CREATED\\sAT", - }, - { - name: "quiet-no-trunc", - args: []string{"--quiet", "--no-trunc", "image:tag"}, - imageHistoryFunc: func(img string) ([]image.HistoryResponseItem, error) { - return []image.HistoryResponseItem{{ - ID: "1234567890123456789", - Created: time.Now().Unix(), - }}, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.NoError(t, err) - actual := buf.String() - if tc.outputRegex == "" { - expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("history-command-success.%s.golden", tc.name))[:]) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) - } else { - match, _ := regexp.MatchString(tc.outputRegex, actual) - assert.True(t, match) - } - } -} diff --git a/cli/command/image/import.go b/cli/command/image/import.go deleted file mode 100644 index 285998f959..0000000000 --- a/cli/command/image/import.go +++ /dev/null @@ -1,88 +0,0 @@ -package image - -import ( - "io" - "os" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - dockeropts "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/pkg/urlutil" - "github.com/spf13/cobra" -) - -type importOptions struct { - source string - reference string - changes dockeropts.ListOpts - message string -} - -// NewImportCommand creates a new `docker import` command -func NewImportCommand(dockerCli command.Cli) *cobra.Command { - var opts importOptions - - cmd := &cobra.Command{ - Use: "import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]", - Short: "Import the contents from a tarball to create a filesystem image", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.source = args[0] - if len(args) > 1 { - opts.reference = args[1] - } - return runImport(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - opts.changes = dockeropts.NewListOpts(nil) - flags.VarP(&opts.changes, "change", "c", "Apply Dockerfile instruction to the created image") - flags.StringVarP(&opts.message, "message", "m", "", "Set commit message for imported image") - - return cmd -} - -func runImport(dockerCli command.Cli, opts importOptions) error { - var ( - in io.Reader - srcName = opts.source - ) - - if opts.source == "-" { - in = dockerCli.In() - } else if !urlutil.IsURL(opts.source) { - srcName = "-" - file, err := os.Open(opts.source) - if err != nil { - return err - } - defer file.Close() - in = file - } - - source := types.ImageImportSource{ - Source: in, - SourceName: srcName, - } - - options := types.ImageImportOptions{ - Message: opts.message, - Changes: opts.changes.GetAll(), - } - - clnt := dockerCli.Client() - - responseBody, err := clnt.ImageImport(context.Background(), source, opts.reference, options) - if err != nil { - return err - } - defer responseBody.Close() - - return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil) -} diff --git a/cli/command/image/import_test.go b/cli/command/image/import_test.go deleted file mode 100644 index c62d610abc..0000000000 --- a/cli/command/image/import_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package image - -import ( - "bytes" - "io" - "io/ioutil" - "strings" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestNewImportCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) - }{ - { - name: "wrong-args", - args: []string{}, - expectedError: "requires at least 1 argument(s).", - }, - { - name: "import-failed", - args: []string{"testdata/import-command-success.input.txt"}, - expectedError: "something went wrong", - imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) { - return nil, errors.Errorf("something went wrong") - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewImportCommandInvalidFile(t *testing.T) { - cmd := NewImportCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer))) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs([]string{"testdata/import-command-success.unexistent-file"}) - testutil.ErrorContains(t, cmd.Execute(), "testdata/import-command-success.unexistent-file") -} - -func TestNewImportCommandSuccess(t *testing.T) { - testCases := []struct { - name string - args []string - imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) - }{ - { - name: "simple", - args: []string{"testdata/import-command-success.input.txt"}, - }, - { - name: "terminal-source", - args: []string{"-"}, - }, - { - name: "double", - args: []string{"-", "image:local"}, - imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) { - assert.Equal(t, "image:local", ref) - return ioutil.NopCloser(strings.NewReader("")), nil - }, - }, - { - name: "message", - args: []string{"--message", "test message", "-"}, - imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) { - assert.Equal(t, "test message", options.Message) - return ioutil.NopCloser(strings.NewReader("")), nil - }, - }, - { - name: "change", - args: []string{"--change", "ENV DEBUG true", "-"}, - imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) { - assert.Equal(t, "ENV DEBUG true", options.Changes[0]) - return ioutil.NopCloser(strings.NewReader("")), nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - assert.NoError(t, cmd.Execute()) - } -} diff --git a/cli/command/image/inspect.go b/cli/command/image/inspect.go deleted file mode 100644 index fe47310cfe..0000000000 --- a/cli/command/image/inspect.go +++ /dev/null @@ -1,44 +0,0 @@ -package image - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" - "github.com/spf13/cobra" -) - -type inspectOptions struct { - format string - refs []string -} - -// newInspectCommand creates a new cobra.Command for `docker image inspect` -func newInspectCommand(dockerCli command.Cli) *cobra.Command { - var opts inspectOptions - - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] IMAGE [IMAGE...]", - Short: "Display detailed information on one or more images", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.refs = args - return runInspect(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - return cmd -} - -func runInspect(dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - getRefFunc := func(ref string) (interface{}, []byte, error) { - return client.ImageInspectWithRaw(ctx, ref) - } - return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRefFunc) -} diff --git a/cli/command/image/inspect_test.go b/cli/command/image/inspect_test.go deleted file mode 100644 index 7299ad7d86..0000000000 --- a/cli/command/image/inspect_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package image - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestNewInspectCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - }{ - { - name: "wrong-args", - args: []string{}, - expectedError: "requires at least 1 argument(s).", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newInspectCommand(test.NewFakeCli(&fakeClient{}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewInspectCommandSuccess(t *testing.T) { - imageInspectInvocationCount := 0 - testCases := []struct { - name string - args []string - imageCount int - imageInspectFunc func(image string) (types.ImageInspect, []byte, error) - }{ - { - name: "simple", - args: []string{"image"}, - imageCount: 1, - imageInspectFunc: func(image string) (types.ImageInspect, []byte, error) { - imageInspectInvocationCount++ - assert.Equal(t, "image", image) - return types.ImageInspect{}, nil, nil - }, - }, - { - name: "format", - imageCount: 1, - args: []string{"--format='{{.ID}}'", "image"}, - imageInspectFunc: func(image string) (types.ImageInspect, []byte, error) { - imageInspectInvocationCount++ - return types.ImageInspect{ID: image}, nil, nil - }, - }, - { - name: "simple-many", - args: []string{"image1", "image2"}, - imageCount: 2, - imageInspectFunc: func(image string) (types.ImageInspect, []byte, error) { - imageInspectInvocationCount++ - if imageInspectInvocationCount == 1 { - assert.Equal(t, "image1", image) - } else { - assert.Equal(t, "image2", image) - } - return types.ImageInspect{}, nil, nil - }, - }, - } - for _, tc := range testCases { - imageInspectInvocationCount = 0 - buf := new(bytes.Buffer) - cmd := newInspectCommand(test.NewFakeCli(&fakeClient{imageInspectFunc: tc.imageInspectFunc}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.NoError(t, err) - actual := buf.String() - expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("inspect-command-success.%s.golden", tc.name))[:]) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) - assert.Equal(t, imageInspectInvocationCount, tc.imageCount) - } -} diff --git a/cli/command/image/list.go b/cli/command/image/list.go deleted file mode 100644 index 862183832b..0000000000 --- a/cli/command/image/list.go +++ /dev/null @@ -1,96 +0,0 @@ -package image - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" -) - -type imagesOptions struct { - matchName string - - quiet bool - all bool - noTrunc bool - showDigests bool - format string - filter opts.FilterOpt -} - -// NewImagesCommand creates a new `docker images` command -func NewImagesCommand(dockerCli command.Cli) *cobra.Command { - opts := imagesOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "images [OPTIONS] [REPOSITORY[:TAG]]", - Short: "List images", - Args: cli.RequiresMaxArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) > 0 { - opts.matchName = args[0] - } - return runImages(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show numeric IDs") - flags.BoolVarP(&opts.all, "all", "a", false, "Show all images (default hides intermediate images)") - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") - flags.BoolVar(&opts.showDigests, "digests", false, "Show digests") - flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - - return cmd -} - -func newListCommand(dockerCli command.Cli) *cobra.Command { - cmd := *NewImagesCommand(dockerCli) - cmd.Aliases = []string{"images", "list"} - cmd.Use = "ls [OPTIONS] [REPOSITORY[:TAG]]" - return &cmd -} - -func runImages(dockerCli command.Cli, opts imagesOptions) error { - ctx := context.Background() - - filters := opts.filter.Value() - if opts.matchName != "" { - filters.Add("reference", opts.matchName) - } - - options := types.ImageListOptions{ - All: opts.all, - Filters: filters, - } - - images, err := dockerCli.Client().ImageList(ctx, options) - if err != nil { - return err - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().ImagesFormat - } else { - format = formatter.TableFormatKey - } - } - - imageCtx := formatter.ImageContext{ - Context: formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewImageFormat(format, opts.quiet, opts.showDigests), - Trunc: !opts.noTrunc, - }, - Digest: opts.showDigests, - } - return formatter.ImageWrite(imageCtx, images) -} diff --git a/cli/command/image/list_test.go b/cli/command/image/list_test.go deleted file mode 100644 index 39905f8fdd..0000000000 --- a/cli/command/image/list_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package image - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/config/configfile" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestNewImagesCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - imageListFunc func(options types.ImageListOptions) ([]types.ImageSummary, error) - }{ - { - name: "wrong-args", - args: []string{"arg1", "arg2"}, - expectedError: "requires at most 1 argument(s).", - }, - { - name: "failed-list", - expectedError: "something went wrong", - imageListFunc: func(options types.ImageListOptions) ([]types.ImageSummary, error) { - return []types.ImageSummary{{}}, errors.Errorf("something went wrong") - }, - }, - } - for _, tc := range testCases { - cmd := NewImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}, new(bytes.Buffer))) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewImagesCommandSuccess(t *testing.T) { - testCases := []struct { - name string - args []string - imageFormat string - imageListFunc func(options types.ImageListOptions) ([]types.ImageSummary, error) - }{ - { - name: "simple", - }, - { - name: "format", - imageFormat: "raw", - }, - { - name: "quiet-format", - args: []string{"-q"}, - imageFormat: "table", - }, - { - name: "match-name", - args: []string{"image"}, - imageListFunc: func(options types.ImageListOptions) ([]types.ImageSummary, error) { - assert.Equal(t, "image", options.Filters.Get("reference")[0]) - return []types.ImageSummary{{}}, nil - }, - }, - { - name: "filters", - args: []string{"--filter", "name=value"}, - imageListFunc: func(options types.ImageListOptions) ([]types.ImageSummary, error) { - assert.Equal(t, "value", options.Filters.Get("name")[0]) - return []types.ImageSummary{{}}, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}, buf) - cli.SetConfigfile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat}) - cmd := NewImagesCommand(cli) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.NoError(t, err) - actual := buf.String() - expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("list-command-success.%s.golden", tc.name))[:]) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) - } -} - -func TestNewListCommandAlias(t *testing.T) { - cmd := newListCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer))) - assert.True(t, cmd.HasAlias("images")) - assert.True(t, cmd.HasAlias("list")) - assert.False(t, cmd.HasAlias("other")) -} diff --git a/cli/command/image/load.go b/cli/command/image/load.go deleted file mode 100644 index 6bd1f726f7..0000000000 --- a/cli/command/image/load.go +++ /dev/null @@ -1,77 +0,0 @@ -package image - -import ( - "io" - - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/pkg/system" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type loadOptions struct { - input string - quiet bool -} - -// NewLoadCommand creates a new `docker load` command -func NewLoadCommand(dockerCli command.Cli) *cobra.Command { - var opts loadOptions - - cmd := &cobra.Command{ - Use: "load [OPTIONS]", - Short: "Load an image from a tar archive or STDIN", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runLoad(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.StringVarP(&opts.input, "input", "i", "", "Read from tar archive file, instead of STDIN") - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the load output") - - return cmd -} - -func runLoad(dockerCli command.Cli, opts loadOptions) error { - - var input io.Reader = dockerCli.In() - if opts.input != "" { - // We use system.OpenSequential to use sequential file access on Windows, avoiding - // depleting the standby list un-necessarily. On Linux, this equates to a regular os.Open. - file, err := system.OpenSequential(opts.input) - if err != nil { - return err - } - defer file.Close() - input = file - } - - // To avoid getting stuck, verify that a tar file is given either in - // the input flag or through stdin and if not display an error message and exit. - if opts.input == "" && dockerCli.In().IsTerminal() { - return errors.Errorf("requested load from stdin, but stdin is empty") - } - - if !dockerCli.Out().IsTerminal() { - opts.quiet = true - } - response, err := dockerCli.Client().ImageLoad(context.Background(), input, opts.quiet) - if err != nil { - return err - } - defer response.Body.Close() - - if response.Body != nil && response.JSON { - return jsonmessage.DisplayJSONMessagesToStream(response.Body, dockerCli.Out(), nil) - } - - _, err = io.Copy(dockerCli.Out(), response.Body) - return err -} diff --git a/cli/command/image/load_test.go b/cli/command/image/load_test.go deleted file mode 100644 index d5c6c06841..0000000000 --- a/cli/command/image/load_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package image - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "strings" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestNewLoadCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - isTerminalIn bool - expectedError string - imageLoadFunc func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) - }{ - { - name: "wrong-args", - args: []string{"arg"}, - expectedError: "accepts no argument(s).", - }, - { - name: "input-to-terminal", - isTerminalIn: true, - expectedError: "requested load from stdin, but stdin is empty", - }, - { - name: "pull-error", - expectedError: "something went wrong", - imageLoadFunc: func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) { - return types.ImageLoadResponse{}, errors.Errorf("something went wrong") - }, - }, - } - for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}, new(bytes.Buffer)) - cli.In().SetIsTerminal(tc.isTerminalIn) - cmd := NewLoadCommand(cli) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewLoadCommandInvalidInput(t *testing.T) { - expectedError := "open *" - cmd := NewLoadCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer))) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs([]string{"--input", "*"}) - err := cmd.Execute() - testutil.ErrorContains(t, err, expectedError) -} - -func TestNewLoadCommandSuccess(t *testing.T) { - testCases := []struct { - name string - args []string - imageLoadFunc func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) - }{ - { - name: "simple", - imageLoadFunc: func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) { - return types.ImageLoadResponse{Body: ioutil.NopCloser(strings.NewReader("Success"))}, nil - }, - }, - { - name: "json", - imageLoadFunc: func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) { - json := "{\"ID\": \"1\"}" - return types.ImageLoadResponse{ - Body: ioutil.NopCloser(strings.NewReader(json)), - JSON: true, - }, nil - }, - }, - { - name: "input-file", - args: []string{"--input", "testdata/load-command-success.input.txt"}, - imageLoadFunc: func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) { - return types.ImageLoadResponse{Body: ioutil.NopCloser(strings.NewReader("Success"))}, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewLoadCommand(test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.NoError(t, err) - actual := buf.String() - expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("load-command-success.%s.golden", tc.name))[:]) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) - } -} diff --git a/cli/command/image/prune.go b/cli/command/image/prune.go deleted file mode 100644 index 59c225be19..0000000000 --- a/cli/command/image/prune.go +++ /dev/null @@ -1,95 +0,0 @@ -package image - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - units "github.com/docker/go-units" - "github.com/spf13/cobra" -) - -type pruneOptions struct { - force bool - all bool - filter opts.FilterOpt -} - -// NewPruneCommand returns a new cobra prune command for images -func NewPruneCommand(dockerCli command.Cli) *cobra.Command { - opts := pruneOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "prune [OPTIONS]", - Short: "Remove unused images", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - spaceReclaimed, output, err := runPrune(dockerCli, opts) - if err != nil { - return err - } - if output != "" { - fmt.Fprintln(dockerCli.Out(), output) - } - fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) - return nil - }, - Tags: map[string]string{"version": "1.25"}, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") - flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images, not just dangling ones") - flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=')") - - return cmd -} - -const ( - allImageWarning = `WARNING! This will remove all images without at least one container associated to them. -Are you sure you want to continue?` - danglingWarning = `WARNING! This will remove all dangling images. -Are you sure you want to continue?` -) - -func runPrune(dockerCli command.Cli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { - pruneFilters := opts.filter.Value() - pruneFilters.Add("dangling", fmt.Sprintf("%v", !opts.all)) - pruneFilters = command.PruneFilters(dockerCli, pruneFilters) - - warning := danglingWarning - if opts.all { - warning = allImageWarning - } - if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { - return - } - - report, err := dockerCli.Client().ImagesPrune(context.Background(), pruneFilters) - if err != nil { - return - } - - if len(report.ImagesDeleted) > 0 { - output = "Deleted Images:\n" - for _, st := range report.ImagesDeleted { - if st.Untagged != "" { - output += fmt.Sprintln("untagged:", st.Untagged) - } else { - output += fmt.Sprintln("deleted:", st.Deleted) - } - } - spaceReclaimed = report.SpaceReclaimed - } - - return -} - -// RunPrune calls the Image Prune API -// This returns the amount of space reclaimed and a detailed output string -func RunPrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) { - return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter}) -} diff --git a/cli/command/image/prune_test.go b/cli/command/image/prune_test.go deleted file mode 100644 index 2ac51578c8..0000000000 --- a/cli/command/image/prune_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package image - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestNewPruneCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - imagesPruneFunc func(pruneFilter filters.Args) (types.ImagesPruneReport, error) - }{ - { - name: "wrong-args", - args: []string{"something"}, - expectedError: "accepts no argument(s).", - }, - { - name: "prune-error", - args: []string{"--force"}, - expectedError: "something went wrong", - imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) { - return types.ImagesPruneReport{}, errors.Errorf("something went wrong") - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{ - imagesPruneFunc: tc.imagesPruneFunc, - }, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewPruneCommandSuccess(t *testing.T) { - testCases := []struct { - name string - args []string - imagesPruneFunc func(pruneFilter filters.Args) (types.ImagesPruneReport, error) - }{ - { - name: "all", - args: []string{"--all"}, - imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) { - assert.Equal(t, "false", pruneFilter.Get("dangling")[0]) - return types.ImagesPruneReport{}, nil - }, - }, - { - name: "force-deleted", - args: []string{"--force"}, - imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) { - assert.Equal(t, "true", pruneFilter.Get("dangling")[0]) - return types.ImagesPruneReport{ - ImagesDeleted: []types.ImageDeleteResponseItem{{Deleted: "image1"}}, - SpaceReclaimed: 1, - }, nil - }, - }, - { - name: "force-untagged", - args: []string{"--force"}, - imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) { - assert.Equal(t, "true", pruneFilter.Get("dangling")[0]) - return types.ImagesPruneReport{ - ImagesDeleted: []types.ImageDeleteResponseItem{{Untagged: "image1"}}, - SpaceReclaimed: 2, - }, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{ - imagesPruneFunc: tc.imagesPruneFunc, - }, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.NoError(t, err) - actual := buf.String() - expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("prune-command-success.%s.golden", tc.name))[:]) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) - } -} diff --git a/cli/command/image/pull.go b/cli/command/image/pull.go deleted file mode 100644 index 7561e47b1d..0000000000 --- a/cli/command/image/pull.go +++ /dev/null @@ -1,85 +0,0 @@ -package image - -import ( - "fmt" - "strings" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/registry" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type pullOptions struct { - remote string - all bool -} - -// NewPullCommand creates a new `docker pull` command -func NewPullCommand(dockerCli command.Cli) *cobra.Command { - var opts pullOptions - - cmd := &cobra.Command{ - Use: "pull [OPTIONS] NAME[:TAG|@DIGEST]", - Short: "Pull an image or a repository from a registry", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.remote = args[0] - return runPull(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.BoolVarP(&opts.all, "all-tags", "a", false, "Download all tagged images in the repository") - command.AddTrustVerificationFlags(flags) - - return cmd -} - -func runPull(dockerCli command.Cli, opts pullOptions) error { - distributionRef, err := reference.ParseNormalizedNamed(opts.remote) - if err != nil { - return err - } - if opts.all && !reference.IsNameOnly(distributionRef) { - return errors.New("tag can't be used with --all-tags/-a") - } - - if !opts.all && reference.IsNameOnly(distributionRef) { - distributionRef = reference.TagNameOnly(distributionRef) - if tagged, ok := distributionRef.(reference.Tagged); ok { - fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", tagged.Tag()) - } - } - - // Resolve the Repository name from fqn to RepositoryInfo - repoInfo, err := registry.ParseRepositoryInfo(distributionRef) - if err != nil { - return err - } - - ctx := context.Background() - - authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) - requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "pull") - - // Check if reference has a digest - _, isCanonical := distributionRef.(reference.Canonical) - if command.IsTrusted() && !isCanonical { - err = trustedPull(ctx, dockerCli, repoInfo, distributionRef, authConfig, requestPrivilege) - } else { - err = imagePullPrivileged(ctx, dockerCli, authConfig, reference.FamiliarString(distributionRef), requestPrivilege, opts.all) - } - if err != nil { - if strings.Contains(err.Error(), "when fetching 'plugin'") { - return errors.New(err.Error() + " - Use `docker plugin install`") - } - return err - } - - return nil -} diff --git a/cli/command/image/pull_test.go b/cli/command/image/pull_test.go deleted file mode 100644 index b4055dc397..0000000000 --- a/cli/command/image/pull_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package image - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/docker/docker/registry" - "github.com/stretchr/testify/assert" - "golang.org/x/net/context" -) - -func TestNewPullCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - trustedPullFunc func(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, - authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error - }{ - { - name: "wrong-args", - expectedError: "requires exactly 1 argument(s).", - args: []string{}, - }, - { - name: "invalid-name", - expectedError: "invalid reference format: repository name must be lowercase", - args: []string{"UPPERCASE_REPO"}, - }, - { - name: "all-tags-with-tag", - expectedError: "tag can't be used with --all-tags/-a", - args: []string{"--all-tags", "image:tag"}, - }, - { - name: "pull-error", - args: []string{"--disable-content-trust=false", "image:tag"}, - expectedError: "you are not authorized to perform this operation: server returned 401.", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewPullCommand(test.NewFakeCli(&fakeClient{}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewPullCommandSuccess(t *testing.T) { - testCases := []struct { - name string - args []string - trustedPullFunc func(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, - authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error - }{ - { - name: "simple", - args: []string{"image:tag"}, - }, - { - name: "simple-no-tag", - args: []string{"image"}, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewPullCommand(test.NewFakeCli(&fakeClient{}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.NoError(t, err) - actual := buf.String() - expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("pull-command-success.%s.golden", tc.name))[:]) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) - } -} diff --git a/cli/command/image/push.go b/cli/command/image/push.go deleted file mode 100644 index 4f773deb80..0000000000 --- a/cli/command/image/push.go +++ /dev/null @@ -1,61 +0,0 @@ -package image - -import ( - "golang.org/x/net/context" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/registry" - "github.com/spf13/cobra" -) - -// NewPushCommand creates a new `docker push` command -func NewPushCommand(dockerCli command.Cli) *cobra.Command { - cmd := &cobra.Command{ - Use: "push [OPTIONS] NAME[:TAG]", - Short: "Push an image or a repository to a registry", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runPush(dockerCli, args[0]) - }, - } - - flags := cmd.Flags() - - command.AddTrustSigningFlags(flags) - - return cmd -} - -func runPush(dockerCli command.Cli, remote string) error { - ref, err := reference.ParseNormalizedNamed(remote) - if err != nil { - return err - } - - // Resolve the Repository name from fqn to RepositoryInfo - repoInfo, err := registry.ParseRepositoryInfo(ref) - if err != nil { - return err - } - - ctx := context.Background() - - // Resolve the Auth config relevant for this server - authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) - requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "push") - - if command.IsTrusted() { - return trustedPush(ctx, dockerCli, repoInfo, ref, authConfig, requestPrivilege) - } - - responseBody, err := imagePushPrivileged(ctx, dockerCli, authConfig, ref, requestPrivilege) - if err != nil { - return err - } - - defer responseBody.Close() - return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil) -} diff --git a/cli/command/image/push_test.go b/cli/command/image/push_test.go deleted file mode 100644 index 9ecb9db680..0000000000 --- a/cli/command/image/push_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package image - -import ( - "bytes" - "io" - "io/ioutil" - "strings" - "testing" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/registry" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "golang.org/x/net/context" -) - -func TestNewPushCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - imagePushFunc func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) - }{ - { - name: "wrong-args", - args: []string{}, - expectedError: "requires exactly 1 argument(s).", - }, - { - name: "invalid-name", - args: []string{"UPPERCASE_REPO"}, - expectedError: "invalid reference format: repository name must be lowercase", - }, - { - name: "push-failed", - args: []string{"image:repo"}, - expectedError: "Failed to push", - imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) { - return ioutil.NopCloser(strings.NewReader("")), errors.Errorf("Failed to push") - }, - }, - { - name: "trust-error", - args: []string{"--disable-content-trust=false", "image:repo"}, - expectedError: "you are not authorized to perform this operation: server returned 401.", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewPushCommand(test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc}, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewPushCommandSuccess(t *testing.T) { - testCases := []struct { - name string - args []string - trustedPushFunc func(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, - ref reference.Named, authConfig types.AuthConfig, - requestPrivilege types.RequestPrivilegeFunc) error - }{ - { - name: "simple", - args: []string{"image:tag"}, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewPushCommand(test.NewFakeCli(&fakeClient{ - imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) { - return ioutil.NopCloser(strings.NewReader("")), nil - }, - }, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - assert.NoError(t, cmd.Execute()) - } -} diff --git a/cli/command/image/remove.go b/cli/command/image/remove.go deleted file mode 100644 index 0680ed05f4..0000000000 --- a/cli/command/image/remove.go +++ /dev/null @@ -1,78 +0,0 @@ -package image - -import ( - "fmt" - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type removeOptions struct { - force bool - noPrune bool -} - -// NewRemoveCommand creates a new `docker remove` command -func NewRemoveCommand(dockerCli command.Cli) *cobra.Command { - var opts removeOptions - - cmd := &cobra.Command{ - Use: "rmi [OPTIONS] IMAGE [IMAGE...]", - Short: "Remove one or more images", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(dockerCli, opts, args) - }, - } - - flags := cmd.Flags() - - flags.BoolVarP(&opts.force, "force", "f", false, "Force removal of the image") - flags.BoolVar(&opts.noPrune, "no-prune", false, "Do not delete untagged parents") - - return cmd -} - -func newRemoveCommand(dockerCli command.Cli) *cobra.Command { - cmd := *NewRemoveCommand(dockerCli) - cmd.Aliases = []string{"rmi", "remove"} - cmd.Use = "rm [OPTIONS] IMAGE [IMAGE...]" - return &cmd -} - -func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error { - client := dockerCli.Client() - ctx := context.Background() - - options := types.ImageRemoveOptions{ - Force: opts.force, - PruneChildren: !opts.noPrune, - } - - var errs []string - for _, image := range images { - dels, err := client.ImageRemove(ctx, image, options) - if err != nil { - errs = append(errs, err.Error()) - } else { - for _, del := range dels { - if del.Deleted != "" { - fmt.Fprintf(dockerCli.Out(), "Deleted: %s\n", del.Deleted) - } else { - fmt.Fprintf(dockerCli.Out(), "Untagged: %s\n", del.Untagged) - } - } - } - } - - if len(errs) > 0 { - return errors.Errorf("%s", strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/image/remove_test.go b/cli/command/image/remove_test.go deleted file mode 100644 index a1b3c4b9b6..0000000000 --- a/cli/command/image/remove_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package image - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestNewRemoveCommandAlias(t *testing.T) { - cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer))) - assert.True(t, cmd.HasAlias("rmi")) - assert.True(t, cmd.HasAlias("remove")) - assert.False(t, cmd.HasAlias("other")) -} - -func TestNewRemoveCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) - }{ - { - name: "wrong args", - expectedError: "requires at least 1 argument(s).", - }, - { - name: "ImageRemove fail", - args: []string{"arg1"}, - expectedError: "error removing image", - imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { - assert.False(t, options.Force) - assert.True(t, options.PruneChildren) - return []types.ImageDeleteResponseItem{}, errors.Errorf("error removing image") - }, - }, - } - for _, tc := range testCases { - cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{ - imageRemoveFunc: tc.imageRemoveFunc, - }, new(bytes.Buffer))) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewRemoveCommandSuccess(t *testing.T) { - testCases := []struct { - name string - args []string - imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) - }{ - { - name: "Image Deleted", - args: []string{"image1"}, - imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { - assert.Equal(t, "image1", image) - return []types.ImageDeleteResponseItem{{Deleted: image}}, nil - }, - }, - { - name: "Image Untagged", - args: []string{"image1"}, - imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { - assert.Equal(t, "image1", image) - return []types.ImageDeleteResponseItem{{Untagged: image}}, nil - }, - }, - { - name: "Image Deleted and Untagged", - args: []string{"image1", "image2"}, - imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { - if image == "image1" { - return []types.ImageDeleteResponseItem{{Untagged: image}}, nil - } - return []types.ImageDeleteResponseItem{{Deleted: image}}, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{ - imageRemoveFunc: tc.imageRemoveFunc, - }, buf)) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - assert.NoError(t, cmd.Execute()) - err := cmd.Execute() - assert.NoError(t, err) - actual := buf.String() - expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("remove-command-success.%s.golden", tc.name))[:]) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected) - } -} diff --git a/cli/command/image/save.go b/cli/command/image/save.go deleted file mode 100644 index d67ccb7d2c..0000000000 --- a/cli/command/image/save.go +++ /dev/null @@ -1,56 +0,0 @@ -package image - -import ( - "io" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type saveOptions struct { - images []string - output string -} - -// NewSaveCommand creates a new `docker save` command -func NewSaveCommand(dockerCli command.Cli) *cobra.Command { - var opts saveOptions - - cmd := &cobra.Command{ - Use: "save [OPTIONS] IMAGE [IMAGE...]", - Short: "Save one or more images to a tar archive (streamed to STDOUT by default)", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.images = args - return runSave(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT") - - return cmd -} - -func runSave(dockerCli command.Cli, opts saveOptions) error { - if opts.output == "" && dockerCli.Out().IsTerminal() { - return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.") - } - - responseBody, err := dockerCli.Client().ImageSave(context.Background(), opts.images) - if err != nil { - return err - } - defer responseBody.Close() - - if opts.output == "" { - _, err := io.Copy(dockerCli.Out(), responseBody) - return err - } - - return command.CopyToFile(opts.output, responseBody) -} diff --git a/cli/command/image/save_test.go b/cli/command/image/save_test.go deleted file mode 100644 index 75475a2b3b..0000000000 --- a/cli/command/image/save_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package image - -import ( - "bytes" - "io" - "io/ioutil" - "os" - "strings" - "testing" - - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewSaveCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - isTerminal bool - expectedError string - imageSaveFunc func(images []string) (io.ReadCloser, error) - }{ - { - name: "wrong args", - args: []string{}, - expectedError: "requires at least 1 argument(s).", - }, - { - name: "output to terminal", - args: []string{"output", "file", "arg1"}, - isTerminal: true, - expectedError: "Cowardly refusing to save to a terminal. Use the -o flag or redirect.", - }, - { - name: "ImageSave fail", - args: []string{"arg1"}, - isTerminal: false, - expectedError: "error saving image", - imageSaveFunc: func(images []string) (io.ReadCloser, error) { - return ioutil.NopCloser(strings.NewReader("")), errors.Errorf("error saving image") - }, - }, - } - for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc}, new(bytes.Buffer)) - cli.Out().SetIsTerminal(tc.isTerminal) - cmd := NewSaveCommand(cli) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNewSaveCommandSuccess(t *testing.T) { - testCases := []struct { - args []string - isTerminal bool - imageSaveFunc func(images []string) (io.ReadCloser, error) - deferredFunc func() - }{ - { - args: []string{"-o", "save_tmp_file", "arg1"}, - isTerminal: true, - imageSaveFunc: func(images []string) (io.ReadCloser, error) { - require.Len(t, images, 1) - assert.Equal(t, "arg1", images[0]) - return ioutil.NopCloser(strings.NewReader("")), nil - }, - deferredFunc: func() { - os.Remove("save_tmp_file") - }, - }, - { - args: []string{"arg1", "arg2"}, - isTerminal: false, - imageSaveFunc: func(images []string) (io.ReadCloser, error) { - require.Len(t, images, 2) - assert.Equal(t, "arg1", images[0]) - assert.Equal(t, "arg2", images[1]) - return ioutil.NopCloser(strings.NewReader("")), nil - }, - }, - } - for _, tc := range testCases { - cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{ - imageSaveFunc: func(images []string) (io.ReadCloser, error) { - return ioutil.NopCloser(strings.NewReader("")), nil - }, - }, new(bytes.Buffer))) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tc.args) - assert.NoError(t, cmd.Execute()) - if tc.deferredFunc != nil { - tc.deferredFunc() - } - } -} diff --git a/cli/command/image/tag.go b/cli/command/image/tag.go deleted file mode 100644 index 409d83afee..0000000000 --- a/cli/command/image/tag.go +++ /dev/null @@ -1,41 +0,0 @@ -package image - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -type tagOptions struct { - image string - name string -} - -// NewTagCommand creates a new `docker tag` command -func NewTagCommand(dockerCli command.Cli) *cobra.Command { - var opts tagOptions - - cmd := &cobra.Command{ - Use: "tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]", - Short: "Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE", - Args: cli.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - opts.image = args[0] - opts.name = args[1] - return runTag(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.SetInterspersed(false) - - return cmd -} - -func runTag(dockerCli command.Cli, opts tagOptions) error { - ctx := context.Background() - - return dockerCli.Client().ImageTag(ctx, opts.image, opts.name) -} diff --git a/cli/command/image/tag_test.go b/cli/command/image/tag_test.go deleted file mode 100644 index b37bf30753..0000000000 --- a/cli/command/image/tag_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package image - -import ( - "bytes" - "io/ioutil" - "testing" - - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/stretchr/testify/assert" -) - -func TestCliNewTagCommandErrors(t *testing.T) { - testCases := [][]string{ - {}, - {"image1"}, - {"image1", "image2", "image3"}, - } - expectedError := "\"tag\" requires exactly 2 argument(s)." - buf := new(bytes.Buffer) - for _, args := range testCases { - cmd := NewTagCommand(test.NewFakeCli(&fakeClient{}, buf)) - cmd.SetArgs(args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), expectedError) - } -} - -func TestCliNewTagCommand(t *testing.T) { - buf := new(bytes.Buffer) - cmd := NewTagCommand( - test.NewFakeCli(&fakeClient{ - imageTagFunc: func(image string, ref string) error { - assert.Equal(t, "image1", image) - assert.Equal(t, "image2", ref) - return nil - }, - }, buf)) - cmd.SetArgs([]string{"image1", "image2"}) - cmd.SetOutput(ioutil.Discard) - assert.NoError(t, cmd.Execute()) - value, _ := cmd.Flags().GetBool("interspersed") - assert.False(t, value) -} diff --git a/cli/command/image/testdata/history-command-success.quiet-no-trunc.golden b/cli/command/image/testdata/history-command-success.quiet-no-trunc.golden deleted file mode 100644 index 65103f6354..0000000000 --- a/cli/command/image/testdata/history-command-success.quiet-no-trunc.golden +++ /dev/null @@ -1 +0,0 @@ -1234567890123456789 diff --git a/cli/command/image/testdata/history-command-success.quiet.golden b/cli/command/image/testdata/history-command-success.quiet.golden deleted file mode 100644 index 42c7c82cc8..0000000000 --- a/cli/command/image/testdata/history-command-success.quiet.golden +++ /dev/null @@ -1 +0,0 @@ -tag diff --git a/cli/command/image/testdata/history-command-success.simple.golden b/cli/command/image/testdata/history-command-success.simple.golden deleted file mode 100644 index 8aa590526f..0000000000 --- a/cli/command/image/testdata/history-command-success.simple.golden +++ /dev/null @@ -1,2 +0,0 @@ -IMAGE CREATED CREATED BY SIZE COMMENT -123456789012 Less than a second ago 0B diff --git a/cli/command/image/testdata/import-command-success.input.txt b/cli/command/image/testdata/import-command-success.input.txt deleted file mode 100644 index 7ab5949b13..0000000000 --- a/cli/command/image/testdata/import-command-success.input.txt +++ /dev/null @@ -1 +0,0 @@ -file input test \ No newline at end of file diff --git a/cli/command/image/testdata/inspect-command-success.format.golden b/cli/command/image/testdata/inspect-command-success.format.golden deleted file mode 100644 index f934996b07..0000000000 --- a/cli/command/image/testdata/inspect-command-success.format.golden +++ /dev/null @@ -1 +0,0 @@ -'image' diff --git a/cli/command/image/testdata/inspect-command-success.simple-many.golden b/cli/command/image/testdata/inspect-command-success.simple-many.golden deleted file mode 100644 index d4042589f8..0000000000 --- a/cli/command/image/testdata/inspect-command-success.simple-many.golden +++ /dev/null @@ -1,50 +0,0 @@ -[ - { - "Id": "", - "RepoTags": null, - "RepoDigests": null, - "Parent": "", - "Comment": "", - "Created": "", - "Container": "", - "ContainerConfig": null, - "DockerVersion": "", - "Author": "", - "Config": null, - "Architecture": "", - "Os": "", - "Size": 0, - "VirtualSize": 0, - "GraphDriver": { - "Data": null, - "Name": "" - }, - "RootFS": { - "Type": "" - } - }, - { - "Id": "", - "RepoTags": null, - "RepoDigests": null, - "Parent": "", - "Comment": "", - "Created": "", - "Container": "", - "ContainerConfig": null, - "DockerVersion": "", - "Author": "", - "Config": null, - "Architecture": "", - "Os": "", - "Size": 0, - "VirtualSize": 0, - "GraphDriver": { - "Data": null, - "Name": "" - }, - "RootFS": { - "Type": "" - } - } -] diff --git a/cli/command/image/testdata/inspect-command-success.simple.golden b/cli/command/image/testdata/inspect-command-success.simple.golden deleted file mode 100644 index 802c52469b..0000000000 --- a/cli/command/image/testdata/inspect-command-success.simple.golden +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "Id": "", - "RepoTags": null, - "RepoDigests": null, - "Parent": "", - "Comment": "", - "Created": "", - "Container": "", - "ContainerConfig": null, - "DockerVersion": "", - "Author": "", - "Config": null, - "Architecture": "", - "Os": "", - "Size": 0, - "VirtualSize": 0, - "GraphDriver": { - "Data": null, - "Name": "" - }, - "RootFS": { - "Type": "" - } - } -] diff --git a/cli/command/image/testdata/list-command-success.filters.golden b/cli/command/image/testdata/list-command-success.filters.golden deleted file mode 100644 index e3b8109bcf..0000000000 --- a/cli/command/image/testdata/list-command-success.filters.golden +++ /dev/null @@ -1 +0,0 @@ -REPOSITORY TAG IMAGE ID CREATED SIZE diff --git a/cli/command/image/testdata/list-command-success.format.golden b/cli/command/image/testdata/list-command-success.format.golden deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/cli/command/image/testdata/list-command-success.match-name.golden b/cli/command/image/testdata/list-command-success.match-name.golden deleted file mode 100644 index e3b8109bcf..0000000000 --- a/cli/command/image/testdata/list-command-success.match-name.golden +++ /dev/null @@ -1 +0,0 @@ -REPOSITORY TAG IMAGE ID CREATED SIZE diff --git a/cli/command/image/testdata/list-command-success.quiet-format.golden b/cli/command/image/testdata/list-command-success.quiet-format.golden deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/cli/command/image/testdata/list-command-success.simple.golden b/cli/command/image/testdata/list-command-success.simple.golden deleted file mode 100644 index e3b8109bcf..0000000000 --- a/cli/command/image/testdata/list-command-success.simple.golden +++ /dev/null @@ -1 +0,0 @@ -REPOSITORY TAG IMAGE ID CREATED SIZE diff --git a/cli/command/image/testdata/load-command-success.input-file.golden b/cli/command/image/testdata/load-command-success.input-file.golden deleted file mode 100644 index 51da4200ab..0000000000 --- a/cli/command/image/testdata/load-command-success.input-file.golden +++ /dev/null @@ -1 +0,0 @@ -Success \ No newline at end of file diff --git a/cli/command/image/testdata/load-command-success.input.txt b/cli/command/image/testdata/load-command-success.input.txt deleted file mode 100644 index 7ab5949b13..0000000000 --- a/cli/command/image/testdata/load-command-success.input.txt +++ /dev/null @@ -1 +0,0 @@ -file input test \ No newline at end of file diff --git a/cli/command/image/testdata/load-command-success.json.golden b/cli/command/image/testdata/load-command-success.json.golden deleted file mode 100644 index c17f16ecd7..0000000000 --- a/cli/command/image/testdata/load-command-success.json.golden +++ /dev/null @@ -1 +0,0 @@ -1: diff --git a/cli/command/image/testdata/load-command-success.simple.golden b/cli/command/image/testdata/load-command-success.simple.golden deleted file mode 100644 index 51da4200ab..0000000000 --- a/cli/command/image/testdata/load-command-success.simple.golden +++ /dev/null @@ -1 +0,0 @@ -Success \ No newline at end of file diff --git a/cli/command/image/testdata/prune-command-success.all.golden b/cli/command/image/testdata/prune-command-success.all.golden deleted file mode 100644 index 4d1445280c..0000000000 --- a/cli/command/image/testdata/prune-command-success.all.golden +++ /dev/null @@ -1,2 +0,0 @@ -WARNING! This will remove all images without at least one container associated to them. -Are you sure you want to continue? [y/N] Total reclaimed space: 0B diff --git a/cli/command/image/testdata/prune-command-success.force-deleted.golden b/cli/command/image/testdata/prune-command-success.force-deleted.golden deleted file mode 100644 index 1b6efd4a99..0000000000 --- a/cli/command/image/testdata/prune-command-success.force-deleted.golden +++ /dev/null @@ -1,4 +0,0 @@ -Deleted Images: -deleted: image1 - -Total reclaimed space: 1B diff --git a/cli/command/image/testdata/prune-command-success.force-untagged.golden b/cli/command/image/testdata/prune-command-success.force-untagged.golden deleted file mode 100644 index 725468fe56..0000000000 --- a/cli/command/image/testdata/prune-command-success.force-untagged.golden +++ /dev/null @@ -1,4 +0,0 @@ -Deleted Images: -untagged: image1 - -Total reclaimed space: 2B diff --git a/cli/command/image/testdata/pull-command-success.simple-no-tag.golden b/cli/command/image/testdata/pull-command-success.simple-no-tag.golden deleted file mode 100644 index 946de409a4..0000000000 --- a/cli/command/image/testdata/pull-command-success.simple-no-tag.golden +++ /dev/null @@ -1 +0,0 @@ -Using default tag: latest diff --git a/cli/command/image/testdata/pull-command-success.simple.golden b/cli/command/image/testdata/pull-command-success.simple.golden deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden b/cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden deleted file mode 100644 index 4efc53719d..0000000000 --- a/cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden +++ /dev/null @@ -1,4 +0,0 @@ -Untagged: image1 -Deleted: image2 -Untagged: image1 -Deleted: image2 diff --git a/cli/command/image/testdata/remove-command-success.Image Deleted.golden b/cli/command/image/testdata/remove-command-success.Image Deleted.golden deleted file mode 100644 index 382724d39f..0000000000 --- a/cli/command/image/testdata/remove-command-success.Image Deleted.golden +++ /dev/null @@ -1,2 +0,0 @@ -Deleted: image1 -Deleted: image1 diff --git a/cli/command/image/testdata/remove-command-success.Image Untagged.golden b/cli/command/image/testdata/remove-command-success.Image Untagged.golden deleted file mode 100644 index c795dac19f..0000000000 --- a/cli/command/image/testdata/remove-command-success.Image Untagged.golden +++ /dev/null @@ -1,2 +0,0 @@ -Untagged: image1 -Untagged: image1 diff --git a/cli/command/image/trust.go b/cli/command/image/trust.go deleted file mode 100644 index 63fb6f09fb..0000000000 --- a/cli/command/image/trust.go +++ /dev/null @@ -1,382 +0,0 @@ -package image - -import ( - "encoding/hex" - "encoding/json" - "fmt" - "io" - "path" - "sort" - - "github.com/Sirupsen/logrus" - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/trust" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/registry" - "github.com/docker/notary/client" - "github.com/docker/notary/tuf/data" - "github.com/opencontainers/go-digest" - "github.com/pkg/errors" - "golang.org/x/net/context" -) - -type target struct { - name string - digest digest.Digest - size int64 -} - -// trustedPush handles content trust pushing of an image -func trustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { - responseBody, err := imagePushPrivileged(ctx, cli, authConfig, ref, requestPrivilege) - if err != nil { - return err - } - - defer responseBody.Close() - - return PushTrustedReference(cli, repoInfo, ref, authConfig, responseBody) -} - -// PushTrustedReference pushes a canonical reference to the trust server. -func PushTrustedReference(cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, in io.Reader) error { - // If it is a trusted push we would like to find the target entry which match the - // tag provided in the function and then do an AddTarget later. - target := &client.Target{} - // Count the times of calling for handleTarget, - // if it is called more that once, that should be considered an error in a trusted push. - cnt := 0 - handleTarget := func(aux *json.RawMessage) { - cnt++ - if cnt > 1 { - // handleTarget should only be called one. This will be treated as an error. - return - } - - var pushResult types.PushResult - err := json.Unmarshal(*aux, &pushResult) - if err == nil && pushResult.Tag != "" { - if dgst, err := digest.Parse(pushResult.Digest); err == nil { - h, err := hex.DecodeString(dgst.Hex()) - if err != nil { - target = nil - return - } - target.Name = pushResult.Tag - target.Hashes = data.Hashes{string(dgst.Algorithm()): h} - target.Length = int64(pushResult.Size) - } - } - } - - var tag string - switch x := ref.(type) { - case reference.Canonical: - return errors.New("cannot push a digest reference") - case reference.NamedTagged: - tag = x.Tag() - default: - // We want trust signatures to always take an explicit tag, - // otherwise it will act as an untrusted push. - if err := jsonmessage.DisplayJSONMessagesToStream(in, cli.Out(), nil); err != nil { - return err - } - fmt.Fprintln(cli.Out(), "No tag specified, skipping trust metadata push") - return nil - } - - if err := jsonmessage.DisplayJSONMessagesToStream(in, cli.Out(), handleTarget); err != nil { - return err - } - - if cnt > 1 { - return errors.Errorf("internal error: only one call to handleTarget expected") - } - - if target == nil { - fmt.Fprintln(cli.Out(), "No targets found, please provide a specific tag in order to sign it") - return nil - } - - fmt.Fprintln(cli.Out(), "Signing and pushing trust metadata") - - repo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "push", "pull") - if err != nil { - fmt.Fprintf(cli.Out(), "Error establishing connection to notary repository: %s\n", err) - return err - } - - // get the latest repository metadata so we can figure out which roles to sign - err = repo.Update(false) - - switch err.(type) { - case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist: - keys := repo.CryptoService.ListKeys(data.CanonicalRootRole) - var rootKeyID string - // always select the first root key - if len(keys) > 0 { - sort.Strings(keys) - rootKeyID = keys[0] - } else { - rootPublicKey, err := repo.CryptoService.Create(data.CanonicalRootRole, "", data.ECDSAKey) - if err != nil { - return err - } - rootKeyID = rootPublicKey.ID() - } - - // Initialize the notary repository with a remotely managed snapshot key - if err := repo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil { - return trust.NotaryError(repoInfo.Name.Name(), err) - } - fmt.Fprintf(cli.Out(), "Finished initializing %q\n", repoInfo.Name.Name()) - err = repo.AddTarget(target, data.CanonicalTargetsRole) - case nil: - // already initialized and we have successfully downloaded the latest metadata - err = addTargetToAllSignableRoles(repo, target) - default: - return trust.NotaryError(repoInfo.Name.Name(), err) - } - - if err == nil { - err = repo.Publish() - } - - if err != nil { - fmt.Fprintf(cli.Out(), "Failed to sign %q:%s - %s\n", repoInfo.Name.Name(), tag, err.Error()) - return trust.NotaryError(repoInfo.Name.Name(), err) - } - - fmt.Fprintf(cli.Out(), "Successfully signed %q:%s\n", repoInfo.Name.Name(), tag) - return nil -} - -// Attempt to add the image target to all the top level delegation roles we can -// (based on whether we have the signing key and whether the role's path allows -// us to). -// If there are no delegation roles, we add to the targets role. -func addTargetToAllSignableRoles(repo *client.NotaryRepository, target *client.Target) error { - var signableRoles []string - - // translate the full key names, which includes the GUN, into just the key IDs - allCanonicalKeyIDs := make(map[string]struct{}) - for fullKeyID := range repo.CryptoService.ListAllKeys() { - allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{} - } - - allDelegationRoles, err := repo.GetDelegationRoles() - if err != nil { - return err - } - - // if there are no delegation roles, then just try to sign it into the targets role - if len(allDelegationRoles) == 0 { - return repo.AddTarget(target, data.CanonicalTargetsRole) - } - - // there are delegation roles, find every delegation role we have a key for, and - // attempt to sign into into all those roles. - for _, delegationRole := range allDelegationRoles { - // We do not support signing any delegation role that isn't a direct child of the targets role. - // Also don't bother checking the keys if we can't add the target - // to this role due to path restrictions - if path.Dir(delegationRole.Name) != data.CanonicalTargetsRole || !delegationRole.CheckPaths(target.Name) { - continue - } - - for _, canonicalKeyID := range delegationRole.KeyIDs { - if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok { - signableRoles = append(signableRoles, delegationRole.Name) - break - } - } - } - - if len(signableRoles) == 0 { - return errors.Errorf("no valid signing keys for delegation roles") - } - - return repo.AddTarget(target, signableRoles...) -} - -// imagePushPrivileged push the image -func imagePushPrivileged(ctx context.Context, cli command.Cli, authConfig types.AuthConfig, ref reference.Named, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) { - encodedAuth, err := command.EncodeAuthToBase64(authConfig) - if err != nil { - return nil, err - } - options := types.ImagePushOptions{ - RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, - } - - return cli.Client().ImagePush(ctx, reference.FamiliarString(ref), options) -} - -// trustedPull handles content trust pulling of an image -func trustedPull(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { - var refs []target - - notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") - if err != nil { - fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) - return err - } - - if tagged, isTagged := ref.(reference.NamedTagged); !isTagged { - // List all targets - targets, err := notaryRepo.ListTargets(trust.ReleasesRole, data.CanonicalTargetsRole) - if err != nil { - return trust.NotaryError(ref.Name(), err) - } - for _, tgt := range targets { - t, err := convertTarget(tgt.Target) - if err != nil { - fmt.Fprintf(cli.Out(), "Skipping target for %q\n", reference.FamiliarName(ref)) - continue - } - // Only list tags in the top level targets role or the releases delegation role - ignore - // all other delegation roles - if tgt.Role != trust.ReleasesRole && tgt.Role != data.CanonicalTargetsRole { - continue - } - refs = append(refs, t) - } - if len(refs) == 0 { - return trust.NotaryError(ref.Name(), errors.Errorf("No trusted tags for %s", ref.Name())) - } - } else { - t, err := notaryRepo.GetTargetByName(tagged.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) - if err != nil { - return trust.NotaryError(ref.Name(), err) - } - // Only get the tag if it's in the top level targets role or the releases delegation role - // ignore it if it's in any other delegation roles - if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { - return trust.NotaryError(ref.Name(), errors.Errorf("No trust data for %s", tagged.Tag())) - } - - logrus.Debugf("retrieving target for %s role\n", t.Role) - r, err := convertTarget(t.Target) - if err != nil { - return err - - } - refs = append(refs, r) - } - - for i, r := range refs { - displayTag := r.name - if displayTag != "" { - displayTag = ":" + displayTag - } - fmt.Fprintf(cli.Out(), "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), reference.FamiliarName(ref), displayTag, r.digest) - - trustedRef, err := reference.WithDigest(reference.TrimNamed(ref), r.digest) - if err != nil { - return err - } - if err := imagePullPrivileged(ctx, cli, authConfig, reference.FamiliarString(trustedRef), requestPrivilege, false); err != nil { - return err - } - - tagged, err := reference.WithTag(reference.TrimNamed(ref), r.name) - if err != nil { - return err - } - - if err := TagTrusted(ctx, cli, trustedRef, tagged); err != nil { - return err - } - } - return nil -} - -// imagePullPrivileged pulls the image and displays it to the output -func imagePullPrivileged(ctx context.Context, cli command.Cli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error { - - encodedAuth, err := command.EncodeAuthToBase64(authConfig) - if err != nil { - return err - } - options := types.ImagePullOptions{ - RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, - All: all, - } - - responseBody, err := cli.Client().ImagePull(ctx, ref, options) - if err != nil { - return err - } - defer responseBody.Close() - - return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil) -} - -// TrustedReference returns the canonical trusted reference for an image reference -func TrustedReference(ctx context.Context, cli command.Cli, ref reference.NamedTagged, rs registry.Service) (reference.Canonical, error) { - var ( - repoInfo *registry.RepositoryInfo - err error - ) - if rs != nil { - repoInfo, err = rs.ResolveRepository(ref) - } else { - repoInfo, err = registry.ParseRepositoryInfo(ref) - } - if err != nil { - return nil, err - } - - // Resolve the Auth config relevant for this server - authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) - - notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") - if err != nil { - fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) - return nil, err - } - - t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) - if err != nil { - return nil, trust.NotaryError(repoInfo.Name.Name(), err) - } - // Only list tags in the top level targets role or the releases delegation role - ignore - // all other delegation roles - if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { - return nil, trust.NotaryError(repoInfo.Name.Name(), errors.Errorf("No trust data for %s", ref.Tag())) - } - r, err := convertTarget(t.Target) - if err != nil { - return nil, err - - } - - return reference.WithDigest(reference.TrimNamed(ref), r.digest) -} - -func convertTarget(t client.Target) (target, error) { - h, ok := t.Hashes["sha256"] - if !ok { - return target{}, errors.New("no valid hash, expecting sha256") - } - return target{ - name: t.Name, - digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)), - size: t.Length, - }, nil -} - -// TagTrusted tags a trusted ref -func TagTrusted(ctx context.Context, cli command.Cli, trustedRef reference.Canonical, ref reference.NamedTagged) error { - // Use familiar references when interacting with client and output - familiarRef := reference.FamiliarString(ref) - trustedFamiliarRef := reference.FamiliarString(trustedRef) - - fmt.Fprintf(cli.Out(), "Tagging %s as %s\n", trustedFamiliarRef, familiarRef) - - return cli.Client().ImageTag(ctx, trustedFamiliarRef, familiarRef) -} diff --git a/cli/command/image/trust_test.go b/cli/command/image/trust_test.go deleted file mode 100644 index 78146465e6..0000000000 --- a/cli/command/image/trust_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package image - -import ( - "os" - "testing" - - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/cli/trust" - "github.com/docker/docker/registry" -) - -func unsetENV() { - os.Unsetenv("DOCKER_CONTENT_TRUST") - os.Unsetenv("DOCKER_CONTENT_TRUST_SERVER") -} - -func TestENVTrustServer(t *testing.T) { - defer unsetENV() - indexInfo := ®istrytypes.IndexInfo{Name: "testserver"} - if err := os.Setenv("DOCKER_CONTENT_TRUST_SERVER", "https://notary-test.com:5000"); err != nil { - t.Fatal("Failed to set ENV variable") - } - output, err := trust.Server(indexInfo) - expectedStr := "https://notary-test.com:5000" - if err != nil || output != expectedStr { - t.Fatalf("Expected server to be %s, got %s", expectedStr, output) - } -} - -func TestHTTPENVTrustServer(t *testing.T) { - defer unsetENV() - indexInfo := ®istrytypes.IndexInfo{Name: "testserver"} - if err := os.Setenv("DOCKER_CONTENT_TRUST_SERVER", "http://notary-test.com:5000"); err != nil { - t.Fatal("Failed to set ENV variable") - } - _, err := trust.Server(indexInfo) - if err == nil { - t.Fatal("Expected error with invalid scheme") - } -} - -func TestOfficialTrustServer(t *testing.T) { - indexInfo := ®istrytypes.IndexInfo{Name: "testserver", Official: true} - output, err := trust.Server(indexInfo) - if err != nil || output != registry.NotaryServer { - t.Fatalf("Expected server to be %s, got %s", registry.NotaryServer, output) - } -} - -func TestNonOfficialTrustServer(t *testing.T) { - indexInfo := ®istrytypes.IndexInfo{Name: "testserver", Official: false} - output, err := trust.Server(indexInfo) - expectedStr := "https://" + indexInfo.Name - if err != nil || output != expectedStr { - t.Fatalf("Expected server to be %s, got %s", expectedStr, output) - } -} diff --git a/cli/command/in.go b/cli/command/in.go deleted file mode 100644 index 54855c6dc2..0000000000 --- a/cli/command/in.go +++ /dev/null @@ -1,56 +0,0 @@ -package command - -import ( - "errors" - "io" - "os" - "runtime" - - "github.com/docker/docker/pkg/term" -) - -// InStream is an input stream used by the DockerCli to read user input -type InStream struct { - CommonStream - in io.ReadCloser -} - -func (i *InStream) Read(p []byte) (int, error) { - return i.in.Read(p) -} - -// Close implements the Closer interface -func (i *InStream) Close() error { - return i.in.Close() -} - -// SetRawTerminal sets raw mode on the input terminal -func (i *InStream) SetRawTerminal() (err error) { - if os.Getenv("NORAW") != "" || !i.CommonStream.isTerminal { - return nil - } - i.CommonStream.state, err = term.SetRawTerminal(i.CommonStream.fd) - return err -} - -// CheckTty checks if we are trying to attach to a container tty -// from a non-tty client input stream, and if so, returns an error. -func (i *InStream) CheckTty(attachStdin, ttyMode bool) error { - // In order to attach to a container tty, input stream for the client must - // be a tty itself: redirecting or piping the client standard input is - // incompatible with `docker run -t`, `docker exec -t` or `docker attach`. - if ttyMode && attachStdin && !i.isTerminal { - eText := "the input device is not a TTY" - if runtime.GOOS == "windows" { - return errors.New(eText + ". If you are using mintty, try prefixing the command with 'winpty'") - } - return errors.New(eText) - } - return nil -} - -// NewInStream returns a new InStream object from a ReadCloser -func NewInStream(in io.ReadCloser) *InStream { - fd, isTerminal := term.GetFdInfo(in) - return &InStream{CommonStream: CommonStream{fd: fd, isTerminal: isTerminal}, in: in} -} diff --git a/cli/command/inspect/inspector.go b/cli/command/inspect/inspector.go deleted file mode 100644 index 13e584ab49..0000000000 --- a/cli/command/inspect/inspector.go +++ /dev/null @@ -1,198 +0,0 @@ -package inspect - -import ( - "bytes" - "encoding/json" - "io" - "strings" - "text/template" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/cli" - "github.com/docker/docker/pkg/templates" - "github.com/pkg/errors" -) - -// Inspector defines an interface to implement to process elements -type Inspector interface { - Inspect(typedElement interface{}, rawElement []byte) error - Flush() error -} - -// TemplateInspector uses a text template to inspect elements. -type TemplateInspector struct { - outputStream io.Writer - buffer *bytes.Buffer - tmpl *template.Template -} - -// NewTemplateInspector creates a new inspector with a template. -func NewTemplateInspector(outputStream io.Writer, tmpl *template.Template) Inspector { - return &TemplateInspector{ - outputStream: outputStream, - buffer: new(bytes.Buffer), - tmpl: tmpl, - } -} - -// NewTemplateInspectorFromString creates a new TemplateInspector from a string -// which is compiled into a template. -func NewTemplateInspectorFromString(out io.Writer, tmplStr string) (Inspector, error) { - if tmplStr == "" { - return NewIndentedInspector(out), nil - } - - tmpl, err := templates.Parse(tmplStr) - if err != nil { - return nil, errors.Errorf("Template parsing error: %s", err) - } - return NewTemplateInspector(out, tmpl), nil -} - -// GetRefFunc is a function which used by Inspect to fetch an object from a -// reference -type GetRefFunc func(ref string) (interface{}, []byte, error) - -// Inspect fetches objects by reference using GetRefFunc and writes the json -// representation to the output writer. -func Inspect(out io.Writer, references []string, tmplStr string, getRef GetRefFunc) error { - inspector, err := NewTemplateInspectorFromString(out, tmplStr) - if err != nil { - return cli.StatusError{StatusCode: 64, Status: err.Error()} - } - - var inspectErrs []string - for _, ref := range references { - element, raw, err := getRef(ref) - if err != nil { - inspectErrs = append(inspectErrs, err.Error()) - continue - } - - if err := inspector.Inspect(element, raw); err != nil { - inspectErrs = append(inspectErrs, err.Error()) - } - } - - if err := inspector.Flush(); err != nil { - logrus.Errorf("%s\n", err) - } - - if len(inspectErrs) != 0 { - return cli.StatusError{ - StatusCode: 1, - Status: strings.Join(inspectErrs, "\n"), - } - } - return nil -} - -// Inspect executes the inspect template. -// It decodes the raw element into a map if the initial execution fails. -// This allows docker cli to parse inspect structs injected with Swarm fields. -func (i *TemplateInspector) Inspect(typedElement interface{}, rawElement []byte) error { - buffer := new(bytes.Buffer) - if err := i.tmpl.Execute(buffer, typedElement); err != nil { - if rawElement == nil { - return errors.Errorf("Template parsing error: %v", err) - } - return i.tryRawInspectFallback(rawElement) - } - i.buffer.Write(buffer.Bytes()) - i.buffer.WriteByte('\n') - return nil -} - -// tryRawInspectFallback executes the inspect template with a raw interface. -// This allows docker cli to parse inspect structs injected with Swarm fields. -func (i *TemplateInspector) tryRawInspectFallback(rawElement []byte) error { - var raw interface{} - buffer := new(bytes.Buffer) - rdr := bytes.NewReader(rawElement) - dec := json.NewDecoder(rdr) - - if rawErr := dec.Decode(&raw); rawErr != nil { - return errors.Errorf("unable to read inspect data: %v", rawErr) - } - - tmplMissingKey := i.tmpl.Option("missingkey=error") - if rawErr := tmplMissingKey.Execute(buffer, raw); rawErr != nil { - return errors.Errorf("Template parsing error: %v", rawErr) - } - - i.buffer.Write(buffer.Bytes()) - i.buffer.WriteByte('\n') - return nil -} - -// Flush writes the result of inspecting all elements into the output stream. -func (i *TemplateInspector) Flush() error { - if i.buffer.Len() == 0 { - _, err := io.WriteString(i.outputStream, "\n") - return err - } - _, err := io.Copy(i.outputStream, i.buffer) - return err -} - -// IndentedInspector uses a buffer to stop the indented representation of an element. -type IndentedInspector struct { - outputStream io.Writer - elements []interface{} - rawElements [][]byte -} - -// NewIndentedInspector generates a new IndentedInspector. -func NewIndentedInspector(outputStream io.Writer) Inspector { - return &IndentedInspector{ - outputStream: outputStream, - } -} - -// Inspect writes the raw element with an indented json format. -func (i *IndentedInspector) Inspect(typedElement interface{}, rawElement []byte) error { - if rawElement != nil { - i.rawElements = append(i.rawElements, rawElement) - } else { - i.elements = append(i.elements, typedElement) - } - return nil -} - -// Flush writes the result of inspecting all elements into the output stream. -func (i *IndentedInspector) Flush() error { - if len(i.elements) == 0 && len(i.rawElements) == 0 { - _, err := io.WriteString(i.outputStream, "[]\n") - return err - } - - var buffer io.Reader - if len(i.rawElements) > 0 { - bytesBuffer := new(bytes.Buffer) - bytesBuffer.WriteString("[") - for idx, r := range i.rawElements { - bytesBuffer.Write(r) - if idx < len(i.rawElements)-1 { - bytesBuffer.WriteString(",") - } - } - bytesBuffer.WriteString("]") - indented := new(bytes.Buffer) - if err := json.Indent(indented, bytesBuffer.Bytes(), "", " "); err != nil { - return err - } - buffer = indented - } else { - b, err := json.MarshalIndent(i.elements, "", " ") - if err != nil { - return err - } - buffer = bytes.NewReader(b) - } - - if _, err := io.Copy(i.outputStream, buffer); err != nil { - return err - } - _, err := io.WriteString(i.outputStream, "\n") - return err -} diff --git a/cli/command/inspect/inspector_test.go b/cli/command/inspect/inspector_test.go deleted file mode 100644 index 9085230ac5..0000000000 --- a/cli/command/inspect/inspector_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package inspect - -import ( - "bytes" - "strings" - "testing" - - "github.com/docker/docker/pkg/templates" -) - -type testElement struct { - DNS string `json:"Dns"` -} - -func TestTemplateInspectorDefault(t *testing.T) { - b := new(bytes.Buffer) - tmpl, err := templates.Parse("{{.DNS}}") - if err != nil { - t.Fatal(err) - } - i := NewTemplateInspector(b, tmpl) - if err := i.Inspect(testElement{"0.0.0.0"}, nil); err != nil { - t.Fatal(err) - } - - if err := i.Flush(); err != nil { - t.Fatal(err) - } - if b.String() != "0.0.0.0\n" { - t.Fatalf("Expected `0.0.0.0\\n`, got `%s`", b.String()) - } -} - -func TestTemplateInspectorEmpty(t *testing.T) { - b := new(bytes.Buffer) - tmpl, err := templates.Parse("{{.DNS}}") - if err != nil { - t.Fatal(err) - } - i := NewTemplateInspector(b, tmpl) - - if err := i.Flush(); err != nil { - t.Fatal(err) - } - if b.String() != "\n" { - t.Fatalf("Expected `\\n`, got `%s`", b.String()) - } -} - -func TestTemplateInspectorTemplateError(t *testing.T) { - b := new(bytes.Buffer) - tmpl, err := templates.Parse("{{.Foo}}") - if err != nil { - t.Fatal(err) - } - i := NewTemplateInspector(b, tmpl) - - err = i.Inspect(testElement{"0.0.0.0"}, nil) - if err == nil { - t.Fatal("Expected error got nil") - } - - if !strings.HasPrefix(err.Error(), "Template parsing error") { - t.Fatalf("Expected template error, got %v", err) - } -} - -func TestTemplateInspectorRawFallback(t *testing.T) { - b := new(bytes.Buffer) - tmpl, err := templates.Parse("{{.Dns}}") - if err != nil { - t.Fatal(err) - } - i := NewTemplateInspector(b, tmpl) - if err := i.Inspect(testElement{"0.0.0.0"}, []byte(`{"Dns": "0.0.0.0"}`)); err != nil { - t.Fatal(err) - } - - if err := i.Flush(); err != nil { - t.Fatal(err) - } - if b.String() != "0.0.0.0\n" { - t.Fatalf("Expected `0.0.0.0\\n`, got `%s`", b.String()) - } -} - -func TestTemplateInspectorRawFallbackError(t *testing.T) { - b := new(bytes.Buffer) - tmpl, err := templates.Parse("{{.Dns}}") - if err != nil { - t.Fatal(err) - } - i := NewTemplateInspector(b, tmpl) - err = i.Inspect(testElement{"0.0.0.0"}, []byte(`{"Foo": "0.0.0.0"}`)) - if err == nil { - t.Fatal("Expected error got nil") - } - - if !strings.HasPrefix(err.Error(), "Template parsing error") { - t.Fatalf("Expected template error, got %v", err) - } -} - -func TestTemplateInspectorMultiple(t *testing.T) { - b := new(bytes.Buffer) - tmpl, err := templates.Parse("{{.DNS}}") - if err != nil { - t.Fatal(err) - } - i := NewTemplateInspector(b, tmpl) - - if err := i.Inspect(testElement{"0.0.0.0"}, nil); err != nil { - t.Fatal(err) - } - if err := i.Inspect(testElement{"1.1.1.1"}, nil); err != nil { - t.Fatal(err) - } - - if err := i.Flush(); err != nil { - t.Fatal(err) - } - if b.String() != "0.0.0.0\n1.1.1.1\n" { - t.Fatalf("Expected `0.0.0.0\\n1.1.1.1\\n`, got `%s`", b.String()) - } -} - -func TestIndentedInspectorDefault(t *testing.T) { - b := new(bytes.Buffer) - i := NewIndentedInspector(b) - if err := i.Inspect(testElement{"0.0.0.0"}, nil); err != nil { - t.Fatal(err) - } - - if err := i.Flush(); err != nil { - t.Fatal(err) - } - - expected := `[ - { - "Dns": "0.0.0.0" - } -] -` - if b.String() != expected { - t.Fatalf("Expected `%s`, got `%s`", expected, b.String()) - } -} - -func TestIndentedInspectorMultiple(t *testing.T) { - b := new(bytes.Buffer) - i := NewIndentedInspector(b) - if err := i.Inspect(testElement{"0.0.0.0"}, nil); err != nil { - t.Fatal(err) - } - - if err := i.Inspect(testElement{"1.1.1.1"}, nil); err != nil { - t.Fatal(err) - } - - if err := i.Flush(); err != nil { - t.Fatal(err) - } - - expected := `[ - { - "Dns": "0.0.0.0" - }, - { - "Dns": "1.1.1.1" - } -] -` - if b.String() != expected { - t.Fatalf("Expected `%s`, got `%s`", expected, b.String()) - } -} - -func TestIndentedInspectorEmpty(t *testing.T) { - b := new(bytes.Buffer) - i := NewIndentedInspector(b) - - if err := i.Flush(); err != nil { - t.Fatal(err) - } - - expected := "[]\n" - if b.String() != expected { - t.Fatalf("Expected `%s`, got `%s`", expected, b.String()) - } -} - -func TestIndentedInspectorRawElements(t *testing.T) { - b := new(bytes.Buffer) - i := NewIndentedInspector(b) - if err := i.Inspect(testElement{"0.0.0.0"}, []byte(`{"Dns": "0.0.0.0", "Node": "0"}`)); err != nil { - t.Fatal(err) - } - - if err := i.Inspect(testElement{"1.1.1.1"}, []byte(`{"Dns": "1.1.1.1", "Node": "1"}`)); err != nil { - t.Fatal(err) - } - - if err := i.Flush(); err != nil { - t.Fatal(err) - } - - expected := `[ - { - "Dns": "0.0.0.0", - "Node": "0" - }, - { - "Dns": "1.1.1.1", - "Node": "1" - } -] -` - if b.String() != expected { - t.Fatalf("Expected `%s`, got `%s`", expected, b.String()) - } -} diff --git a/cli/command/network/cmd.go b/cli/command/network/cmd.go deleted file mode 100644 index ab8393cded..0000000000 --- a/cli/command/network/cmd.go +++ /dev/null @@ -1,28 +0,0 @@ -package network - -import ( - "github.com/spf13/cobra" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" -) - -// NewNetworkCommand returns a cobra command for `network` subcommands -func NewNetworkCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "network", - Short: "Manage networks", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - } - cmd.AddCommand( - newConnectCommand(dockerCli), - newCreateCommand(dockerCli), - newDisconnectCommand(dockerCli), - newInspectCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - NewPruneCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/network/connect.go b/cli/command/network/connect.go deleted file mode 100644 index bc90ddaba7..0000000000 --- a/cli/command/network/connect.go +++ /dev/null @@ -1,63 +0,0 @@ -package network - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" -) - -type connectOptions struct { - network string - container string - ipaddress string - ipv6address string - links opts.ListOpts - aliases []string - linklocalips []string -} - -func newConnectCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := connectOptions{ - links: opts.NewListOpts(opts.ValidateLink), - } - - cmd := &cobra.Command{ - Use: "connect [OPTIONS] NETWORK CONTAINER", - Short: "Connect a container to a network", - Args: cli.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - opts.network = args[0] - opts.container = args[1] - return runConnect(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVar(&opts.ipaddress, "ip", "", "IPv4 address (e.g., 172.30.100.104)") - flags.StringVar(&opts.ipv6address, "ip6", "", "IPv6 address (e.g., 2001:db8::33)") - flags.Var(&opts.links, "link", "Add link to another container") - flags.StringSliceVar(&opts.aliases, "alias", []string{}, "Add network-scoped alias for the container") - flags.StringSliceVar(&opts.linklocalips, "link-local-ip", []string{}, "Add a link-local address for the container") - - return cmd -} - -func runConnect(dockerCli *command.DockerCli, opts connectOptions) error { - client := dockerCli.Client() - - epConfig := &network.EndpointSettings{ - IPAMConfig: &network.EndpointIPAMConfig{ - IPv4Address: opts.ipaddress, - IPv6Address: opts.ipv6address, - LinkLocalIPs: opts.linklocalips, - }, - Links: opts.links.GetAll(), - Aliases: opts.aliases, - } - - return client.NetworkConnect(context.Background(), opts.network, opts.container, epConfig) -} diff --git a/cli/command/network/create.go b/cli/command/network/create.go deleted file mode 100644 index 90119af919..0000000000 --- a/cli/command/network/create.go +++ /dev/null @@ -1,232 +0,0 @@ -package network - -import ( - "fmt" - "net" - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type createOptions struct { - name string - driver string - driverOpts opts.MapOpts - labels opts.ListOpts - internal bool - ipv6 bool - attachable bool - ingress bool - - ipamDriver string - ipamSubnet []string - ipamIPRange []string - ipamGateway []string - ipamAux opts.MapOpts - ipamOpt opts.MapOpts -} - -func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := createOptions{ - driverOpts: *opts.NewMapOpts(nil, nil), - labels: opts.NewListOpts(opts.ValidateEnv), - ipamAux: *opts.NewMapOpts(nil, nil), - ipamOpt: *opts.NewMapOpts(nil, nil), - } - - cmd := &cobra.Command{ - Use: "create [OPTIONS] NETWORK", - Short: "Create a network", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.name = args[0] - return runCreate(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.driver, "driver", "d", "bridge", "Driver to manage the Network") - flags.VarP(&opts.driverOpts, "opt", "o", "Set driver specific options") - flags.Var(&opts.labels, "label", "Set metadata on a network") - flags.BoolVar(&opts.internal, "internal", false, "Restrict external access to the network") - flags.BoolVar(&opts.ipv6, "ipv6", false, "Enable IPv6 networking") - flags.BoolVar(&opts.attachable, "attachable", false, "Enable manual container attachment") - flags.SetAnnotation("attachable", "version", []string{"1.25"}) - flags.BoolVar(&opts.ingress, "ingress", false, "Create swarm routing-mesh network") - flags.SetAnnotation("ingress", "version", []string{"1.29"}) - - flags.StringVar(&opts.ipamDriver, "ipam-driver", "default", "IP Address Management Driver") - flags.StringSliceVar(&opts.ipamSubnet, "subnet", []string{}, "Subnet in CIDR format that represents a network segment") - flags.StringSliceVar(&opts.ipamIPRange, "ip-range", []string{}, "Allocate container ip from a sub-range") - flags.StringSliceVar(&opts.ipamGateway, "gateway", []string{}, "IPv4 or IPv6 Gateway for the master subnet") - - flags.Var(&opts.ipamAux, "aux-address", "Auxiliary IPv4 or IPv6 addresses used by Network driver") - flags.Var(&opts.ipamOpt, "ipam-opt", "Set IPAM driver specific options") - - return cmd -} - -func runCreate(dockerCli *command.DockerCli, opts createOptions) error { - client := dockerCli.Client() - - ipamCfg, err := consolidateIpam(opts.ipamSubnet, opts.ipamIPRange, opts.ipamGateway, opts.ipamAux.GetAll()) - if err != nil { - return err - } - - // Construct network create request body - nc := types.NetworkCreate{ - Driver: opts.driver, - Options: opts.driverOpts.GetAll(), - IPAM: &network.IPAM{ - Driver: opts.ipamDriver, - Config: ipamCfg, - Options: opts.ipamOpt.GetAll(), - }, - CheckDuplicate: true, - Internal: opts.internal, - EnableIPv6: opts.ipv6, - Attachable: opts.attachable, - Ingress: opts.ingress, - Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()), - } - - resp, err := client.NetworkCreate(context.Background(), opts.name, nc) - if err != nil { - return err - } - fmt.Fprintf(dockerCli.Out(), "%s\n", resp.ID) - return nil -} - -// Consolidates the ipam configuration as a group from different related configurations -// user can configure network with multiple non-overlapping subnets and hence it is -// possible to correlate the various related parameters and consolidate them. -// consolidateIpam consolidates subnets, ip-ranges, gateways and auxiliary addresses into -// structured ipam data. -func consolidateIpam(subnets, ranges, gateways []string, auxaddrs map[string]string) ([]network.IPAMConfig, error) { - if len(subnets) < len(ranges) || len(subnets) < len(gateways) { - return nil, errors.Errorf("every ip-range or gateway must have a corresponding subnet") - } - iData := map[string]*network.IPAMConfig{} - - // Populate non-overlapping subnets into consolidation map - for _, s := range subnets { - for k := range iData { - ok1, err := subnetMatches(s, k) - if err != nil { - return nil, err - } - ok2, err := subnetMatches(k, s) - if err != nil { - return nil, err - } - if ok1 || ok2 { - return nil, errors.Errorf("multiple overlapping subnet configuration is not supported") - } - } - iData[s] = &network.IPAMConfig{Subnet: s, AuxAddress: map[string]string{}} - } - - // Validate and add valid ip ranges - for _, r := range ranges { - match := false - for _, s := range subnets { - ok, err := subnetMatches(s, r) - if err != nil { - return nil, err - } - if !ok { - continue - } - if iData[s].IPRange != "" { - return nil, errors.Errorf("cannot configure multiple ranges (%s, %s) on the same subnet (%s)", r, iData[s].IPRange, s) - } - d := iData[s] - d.IPRange = r - match = true - } - if !match { - return nil, errors.Errorf("no matching subnet for range %s", r) - } - } - - // Validate and add valid gateways - for _, g := range gateways { - match := false - for _, s := range subnets { - ok, err := subnetMatches(s, g) - if err != nil { - return nil, err - } - if !ok { - continue - } - if iData[s].Gateway != "" { - return nil, errors.Errorf("cannot configure multiple gateways (%s, %s) for the same subnet (%s)", g, iData[s].Gateway, s) - } - d := iData[s] - d.Gateway = g - match = true - } - if !match { - return nil, errors.Errorf("no matching subnet for gateway %s", g) - } - } - - // Validate and add aux-addresses - for key, aa := range auxaddrs { - match := false - for _, s := range subnets { - ok, err := subnetMatches(s, aa) - if err != nil { - return nil, err - } - if !ok { - continue - } - iData[s].AuxAddress[key] = aa - match = true - } - if !match { - return nil, errors.Errorf("no matching subnet for aux-address %s", aa) - } - } - - idl := []network.IPAMConfig{} - for _, v := range iData { - idl = append(idl, *v) - } - return idl, nil -} - -func subnetMatches(subnet, data string) (bool, error) { - var ( - ip net.IP - ) - - _, s, err := net.ParseCIDR(subnet) - if err != nil { - return false, errors.Errorf("Invalid subnet %s : %v", s, err) - } - - if strings.Contains(data, "/") { - ip, _, err = net.ParseCIDR(data) - if err != nil { - return false, errors.Errorf("Invalid cidr %s : %v", data, err) - } - } else { - ip = net.ParseIP(data) - } - - return s.Contains(ip), nil -} diff --git a/cli/command/network/disconnect.go b/cli/command/network/disconnect.go deleted file mode 100644 index c9d9c14a13..0000000000 --- a/cli/command/network/disconnect.go +++ /dev/null @@ -1,41 +0,0 @@ -package network - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -type disconnectOptions struct { - network string - container string - force bool -} - -func newDisconnectCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := disconnectOptions{} - - cmd := &cobra.Command{ - Use: "disconnect [OPTIONS] NETWORK CONTAINER", - Short: "Disconnect a container from a network", - Args: cli.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - opts.network = args[0] - opts.container = args[1] - return runDisconnect(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Force the container to disconnect from a network") - - return cmd -} - -func runDisconnect(dockerCli *command.DockerCli, opts disconnectOptions) error { - client := dockerCli.Client() - - return client.NetworkDisconnect(context.Background(), opts.network, opts.container, opts.force) -} diff --git a/cli/command/network/inspect.go b/cli/command/network/inspect.go deleted file mode 100644 index e58d66b77a..0000000000 --- a/cli/command/network/inspect.go +++ /dev/null @@ -1,47 +0,0 @@ -package network - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" - "github.com/spf13/cobra" -) - -type inspectOptions struct { - format string - names []string - verbose bool -} - -func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts inspectOptions - - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] NETWORK [NETWORK...]", - Short: "Display detailed information on one or more networks", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.names = args - return runInspect(dockerCli, opts) - }, - } - - cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - cmd.Flags().BoolVarP(&opts.verbose, "verbose", "v", false, "Verbose output for diagnostics") - - return cmd -} - -func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { - client := dockerCli.Client() - - ctx := context.Background() - - getNetFunc := func(name string) (interface{}, []byte, error) { - return client.NetworkInspectWithRaw(ctx, name, opts.verbose) - } - - return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getNetFunc) -} diff --git a/cli/command/network/list.go b/cli/command/network/list.go deleted file mode 100644 index 1a5d285103..0000000000 --- a/cli/command/network/list.go +++ /dev/null @@ -1,76 +0,0 @@ -package network - -import ( - "sort" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" -) - -type byNetworkName []types.NetworkResource - -func (r byNetworkName) Len() int { return len(r) } -func (r byNetworkName) Swap(i, j int) { r[i], r[j] = r[j], r[i] } -func (r byNetworkName) Less(i, j int) bool { return r[i].Name < r[j].Name } - -type listOptions struct { - quiet bool - noTrunc bool - format string - filter opts.FilterOpt -} - -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := listOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ls [OPTIONS]", - Aliases: []string{"list"}, - Short: "List networks", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display network IDs") - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate the output") - flags.StringVar(&opts.format, "format", "", "Pretty-print networks using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Provide filter values (e.g. 'driver=bridge')") - - return cmd -} - -func runList(dockerCli *command.DockerCli, opts listOptions) error { - client := dockerCli.Client() - options := types.NetworkListOptions{Filters: opts.filter.Value()} - networkResources, err := client.NetworkList(context.Background(), options) - if err != nil { - return err - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().NetworksFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().NetworksFormat - } else { - format = formatter.TableFormatKey - } - } - - sort.Sort(byNetworkName(networkResources)) - - networksCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewNetworkFormat(format, opts.quiet), - Trunc: !opts.noTrunc, - } - return formatter.NetworkWrite(networksCtx, networkResources) -} diff --git a/cli/command/network/prune.go b/cli/command/network/prune.go deleted file mode 100644 index ec363ab914..0000000000 --- a/cli/command/network/prune.go +++ /dev/null @@ -1,77 +0,0 @@ -package network - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" -) - -type pruneOptions struct { - force bool - filter opts.FilterOpt -} - -// NewPruneCommand returns a new cobra prune command for networks -func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := pruneOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "prune [OPTIONS]", - Short: "Remove all unused networks", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - output, err := runPrune(dockerCli, opts) - if err != nil { - return err - } - if output != "" { - fmt.Fprintln(dockerCli.Out(), output) - } - return nil - }, - Tags: map[string]string{"version": "1.25"}, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") - flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=')") - - return cmd -} - -const warning = `WARNING! This will remove all networks not used by at least one container. -Are you sure you want to continue?` - -func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (output string, err error) { - pruneFilters := command.PruneFilters(dockerCli, opts.filter.Value()) - - if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { - return - } - - report, err := dockerCli.Client().NetworksPrune(context.Background(), pruneFilters) - if err != nil { - return - } - - if len(report.NetworksDeleted) > 0 { - output = "Deleted Networks:\n" - for _, id := range report.NetworksDeleted { - output += id + "\n" - } - } - - return -} - -// RunPrune calls the Network Prune API -// This returns the amount of space reclaimed and a detailed output string -func RunPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { - output, err := runPrune(dockerCli, pruneOptions{force: true, filter: filter}) - return 0, output, err -} diff --git a/cli/command/network/remove.go b/cli/command/network/remove.go deleted file mode 100644 index b5f074a981..0000000000 --- a/cli/command/network/remove.go +++ /dev/null @@ -1,53 +0,0 @@ -package network - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { - return &cobra.Command{ - Use: "rm NETWORK [NETWORK...]", - Aliases: []string{"remove"}, - Short: "Remove one or more networks", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(dockerCli, args) - }, - } -} - -const ingressWarning = "WARNING! Before removing the routing-mesh network, " + - "make sure all the nodes in your swarm run the same docker engine version. " + - "Otherwise, removal may not be effective and functionality of newly create " + - "ingress networks will be impaired.\nAre you sure you want to continue?" - -func runRemove(dockerCli *command.DockerCli, networks []string) error { - client := dockerCli.Client() - ctx := context.Background() - status := 0 - - for _, name := range networks { - if nw, _, err := client.NetworkInspectWithRaw(ctx, name, false); err == nil && - nw.Ingress && - !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), ingressWarning) { - continue - } - if err := client.NetworkRemove(ctx, name); err != nil { - fmt.Fprintf(dockerCli.Err(), "%s\n", err) - status = 1 - continue - } - fmt.Fprintf(dockerCli.Out(), "%s\n", name) - } - - if status != 0 { - return cli.StatusError{StatusCode: status} - } - return nil -} diff --git a/cli/command/node/client_test.go b/cli/command/node/client_test.go deleted file mode 100644 index 1f5cdc7cee..0000000000 --- a/cli/command/node/client_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package node - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -type fakeClient struct { - client.Client - infoFunc func() (types.Info, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - nodeListFunc func() ([]swarm.Node, error) - nodeRemoveFunc func() error - nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error - taskInspectFunc func(taskID string) (swarm.Task, []byte, error) - taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error) -} - -func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, ref string) (swarm.Node, []byte, error) { - if cli.nodeInspectFunc != nil { - return cli.nodeInspectFunc() - } - return swarm.Node{}, []byte{}, nil -} - -func (cli *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) { - if cli.nodeListFunc != nil { - return cli.nodeListFunc() - } - return []swarm.Node{}, nil -} - -func (cli *fakeClient) NodeRemove(ctx context.Context, nodeID string, options types.NodeRemoveOptions) error { - if cli.nodeRemoveFunc != nil { - return cli.nodeRemoveFunc() - } - return nil -} - -func (cli *fakeClient) NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if cli.nodeUpdateFunc != nil { - return cli.nodeUpdateFunc(nodeID, version, node) - } - return nil -} - -func (cli *fakeClient) Info(ctx context.Context) (types.Info, error) { - if cli.infoFunc != nil { - return cli.infoFunc() - } - return types.Info{}, nil -} - -func (cli *fakeClient) TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) { - if cli.taskInspectFunc != nil { - return cli.taskInspectFunc(taskID) - } - return swarm.Task{}, []byte{}, nil -} - -func (cli *fakeClient) TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) { - if cli.taskListFunc != nil { - return cli.taskListFunc(options) - } - return []swarm.Task{}, nil -} diff --git a/cli/command/node/cmd.go b/cli/command/node/cmd.go deleted file mode 100644 index ea8b40a9a6..0000000000 --- a/cli/command/node/cmd.go +++ /dev/null @@ -1,57 +0,0 @@ -package node - -import ( - "errors" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - apiclient "github.com/docker/docker/client" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -// NewNodeCommand returns a cobra command for `node` subcommands -func NewNodeCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "node", - Short: "Manage Swarm nodes", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - Tags: map[string]string{"version": "1.24"}, - } - cmd.AddCommand( - newDemoteCommand(dockerCli), - newInspectCommand(dockerCli), - newListCommand(dockerCli), - newPromoteCommand(dockerCli), - newRemoveCommand(dockerCli), - newPsCommand(dockerCli), - newUpdateCommand(dockerCli), - ) - return cmd -} - -// Reference returns the reference of a node. The special value "self" for a node -// reference is mapped to the current node, hence the node ID is retrieved using -// the `/info` endpoint. -func Reference(ctx context.Context, client apiclient.APIClient, ref string) (string, error) { - if ref == "self" { - info, err := client.Info(ctx) - if err != nil { - return "", err - } - if info.Swarm.NodeID == "" { - // If there's no node ID in /info, the node probably - // isn't a manager. Call a swarm-specific endpoint to - // get a more specific error message. - _, err = client.NodeList(ctx, types.NodeListOptions{}) - if err != nil { - return "", err - } - return "", errors.New("node ID not found in /info") - } - return info.Swarm.NodeID, nil - } - return ref, nil -} diff --git a/cli/command/node/demote.go b/cli/command/node/demote.go deleted file mode 100644 index 72ed3ea630..0000000000 --- a/cli/command/node/demote.go +++ /dev/null @@ -1,36 +0,0 @@ -package node - -import ( - "fmt" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -func newDemoteCommand(dockerCli command.Cli) *cobra.Command { - return &cobra.Command{ - Use: "demote NODE [NODE...]", - Short: "Demote one or more nodes from manager in the swarm", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runDemote(dockerCli, args) - }, - } -} - -func runDemote(dockerCli command.Cli, nodes []string) error { - demote := func(node *swarm.Node) error { - if node.Spec.Role == swarm.NodeRoleWorker { - fmt.Fprintf(dockerCli.Out(), "Node %s is already a worker.\n", node.ID) - return errNoRoleChange - } - node.Spec.Role = swarm.NodeRoleWorker - return nil - } - success := func(nodeID string) { - fmt.Fprintf(dockerCli.Out(), "Manager %s demoted in the swarm.\n", nodeID) - } - return updateNodes(dockerCli, nodes, demote, success) -} diff --git a/cli/command/node/demote_test.go b/cli/command/node/demote_test.go deleted file mode 100644 index 803b9c229b..0000000000 --- a/cli/command/node/demote_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package node - -import ( - "bytes" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/stretchr/testify/assert" -) - -func TestNodeDemoteErrors(t *testing.T) { - testCases := []struct { - args []string - nodeInspectFunc func() (swarm.Node, []byte, error) - nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error - expectedError string - }{ - { - expectedError: "requires at least 1 argument", - }, - { - args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") - }, - expectedError: "error inspecting the node", - }, - { - args: []string{"nodeID"}, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - return errors.Errorf("error updating the node") - }, - expectedError: "error updating the node", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newDemoteCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: tc.nodeInspectFunc, - nodeUpdateFunc: tc.nodeUpdateFunc, - }, buf)) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNodeDemoteNoChange(t *testing.T) { - buf := new(bytes.Buffer) - cmd := newDemoteCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Role != swarm.NodeRoleWorker { - return errors.Errorf("expected role worker, got %s", node.Role) - } - return nil - }, - }, buf)) - cmd.SetArgs([]string{"nodeID"}) - assert.NoError(t, cmd.Execute()) -} - -func TestNodeDemoteMultipleNode(t *testing.T) { - buf := new(bytes.Buffer) - cmd := newDemoteCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager()), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Role != swarm.NodeRoleWorker { - return errors.Errorf("expected role worker, got %s", node.Role) - } - return nil - }, - }, buf)) - cmd.SetArgs([]string{"nodeID1", "nodeID2"}) - assert.NoError(t, cmd.Execute()) -} diff --git a/cli/command/node/inspect.go b/cli/command/node/inspect.go deleted file mode 100644 index 39b90bb72e..0000000000 --- a/cli/command/node/inspect.go +++ /dev/null @@ -1,72 +0,0 @@ -package node - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type inspectOptions struct { - nodeIds []string - format string - pretty bool -} - -func newInspectCommand(dockerCli command.Cli) *cobra.Command { - var opts inspectOptions - - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] self|NODE [NODE...]", - Short: "Display detailed information on one or more nodes", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.nodeIds = args - return runInspect(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format") - return cmd -} - -func runInspect(dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - if opts.pretty { - opts.format = "pretty" - } - - getRef := func(ref string) (interface{}, []byte, error) { - nodeRef, err := Reference(ctx, client, ref) - if err != nil { - return nil, nil, err - } - node, _, err := client.NodeInspectWithRaw(ctx, nodeRef) - return node, nil, err - } - f := opts.format - - // check if the user is trying to apply a template to the pretty format, which - // is not supported - if strings.HasPrefix(f, "pretty") && f != "pretty" { - return fmt.Errorf("Cannot supply extra formatting options to the pretty template") - } - - nodeCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewNodeFormat(f, false), - } - - if err := formatter.NodeInspectWrite(nodeCtx, opts.nodeIds, getRef); err != nil { - return cli.StatusError{StatusCode: 1, Status: err.Error()} - } - return nil -} diff --git a/cli/command/node/inspect_test.go b/cli/command/node/inspect_test.go deleted file mode 100644 index 95b45d51e9..0000000000 --- a/cli/command/node/inspect_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package node - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestNodeInspectErrors(t *testing.T) { - testCases := []struct { - args []string - flags map[string]string - nodeInspectFunc func() (swarm.Node, []byte, error) - infoFunc func() (types.Info, error) - expectedError string - }{ - { - expectedError: "requires at least 1 argument", - }, - { - args: []string{"self"}, - infoFunc: func() (types.Info, error) { - return types.Info{}, errors.Errorf("error asking for node info") - }, - expectedError: "error asking for node info", - }, - { - args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") - }, - infoFunc: func() (types.Info, error) { - return types.Info{}, errors.Errorf("error asking for node info") - }, - expectedError: "error inspecting the node", - }, - { - args: []string{"self"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") - }, - infoFunc: func() (types.Info, error) { - return types.Info{Swarm: swarm.Info{NodeID: "abc"}}, nil - }, - expectedError: "error inspecting the node", - }, - { - args: []string{"self"}, - flags: map[string]string{ - "pretty": "true", - }, - infoFunc: func() (types.Info, error) { - return types.Info{}, errors.Errorf("error asking for node info") - }, - expectedError: "error asking for node info", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newInspectCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: tc.nodeInspectFunc, - infoFunc: tc.infoFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNodeInspectPretty(t *testing.T) { - testCases := []struct { - name string - nodeInspectFunc func() (swarm.Node, []byte, error) - }{ - { - name: "simple", - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(NodeLabels(map[string]string{ - "lbl1": "value1", - })), []byte{}, nil - }, - }, - { - name: "manager", - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager()), []byte{}, nil - }, - }, - { - name: "manager-leader", - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager(Leader())), []byte{}, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newInspectCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: tc.nodeInspectFunc, - }, buf)) - cmd.SetArgs([]string{"nodeID"}) - cmd.Flags().Set("pretty", "true") - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("node-inspect-pretty.%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} diff --git a/cli/command/node/list.go b/cli/command/node/list.go deleted file mode 100644 index 9c6224dd19..0000000000 --- a/cli/command/node/list.go +++ /dev/null @@ -1,73 +0,0 @@ -package node - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" -) - -type listOptions struct { - quiet bool - format string - filter opts.FilterOpt -} - -func newListCommand(dockerCli command.Cli) *cobra.Command { - opts := listOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ls [OPTIONS]", - Aliases: []string{"list"}, - Short: "List nodes in the swarm", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, opts) - }, - } - flags := cmd.Flags() - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") - flags.StringVar(&opts.format, "format", "", "Pretty-print nodes using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - - return cmd -} - -func runList(dockerCli command.Cli, opts listOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - nodes, err := client.NodeList( - ctx, - types.NodeListOptions{Filters: opts.filter.Value()}) - if err != nil { - return err - } - - info := types.Info{} - if len(nodes) > 0 && !opts.quiet { - // only non-empty nodes and not quiet, should we call /info api - info, err = client.Info(ctx) - if err != nil { - return err - } - } - - format := opts.format - if len(format) == 0 { - format = formatter.TableFormatKey - if len(dockerCli.ConfigFile().NodesFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().NodesFormat - } - } - - nodesCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewNodeFormat(format, opts.quiet), - } - return formatter.NodeWrite(nodesCtx, nodes, info) -} diff --git a/cli/command/node/list_test.go b/cli/command/node/list_test.go deleted file mode 100644 index af2d6be156..0000000000 --- a/cli/command/node/list_test.go +++ /dev/null @@ -1,162 +0,0 @@ -package node - -import ( - "bytes" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/config/configfile" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/stretchr/testify/assert" -) - -func TestNodeListErrorOnAPIFailure(t *testing.T) { - testCases := []struct { - nodeListFunc func() ([]swarm.Node, error) - infoFunc func() (types.Info, error) - expectedError string - }{ - { - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{}, errors.Errorf("error listing nodes") - }, - expectedError: "error listing nodes", - }, - { - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - { - ID: "nodeID", - }, - }, nil - }, - infoFunc: func() (types.Info, error) { - return types.Info{}, errors.Errorf("error asking for node info") - }, - expectedError: "error asking for node info", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - nodeListFunc: tc.nodeListFunc, - infoFunc: tc.infoFunc, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newListCommand(cli) - cmd.SetOutput(ioutil.Discard) - assert.EqualError(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNodeList(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - *Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())), - *Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()), - *Node(NodeID("nodeID3"), Hostname("nodeHostname3")), - }, nil - }, - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - NodeID: "nodeID1", - }, - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newListCommand(cli) - assert.NoError(t, cmd.Execute()) - assert.Contains(t, buf.String(), `nodeID1 * nodeHostname1 Ready Active Leader`) - assert.Contains(t, buf.String(), `nodeID2 nodeHostname2 Ready Active Reachable`) - assert.Contains(t, buf.String(), `nodeID3 nodeHostname3 Ready Active`) -} - -func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - *Node(), - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newListCommand(cli) - cmd.Flags().Set("quiet", "true") - assert.NoError(t, cmd.Execute()) - assert.Contains(t, buf.String(), "nodeID") -} - -// Test case for #24090 -func TestNodeListContainsHostname(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{}, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newListCommand(cli) - assert.NoError(t, cmd.Execute()) - assert.Contains(t, buf.String(), "HOSTNAME") -} - -func TestNodeListDefaultFormat(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - *Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())), - *Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()), - *Node(NodeID("nodeID3"), Hostname("nodeHostname3")), - }, nil - }, - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - NodeID: "nodeID1", - }, - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{ - NodesFormat: "{{.ID}}: {{.Hostname}} {{.Status}}/{{.ManagerStatus}}", - }) - cmd := newListCommand(cli) - assert.NoError(t, cmd.Execute()) - assert.Contains(t, buf.String(), `nodeID1: nodeHostname1 Ready/Leader`) - assert.Contains(t, buf.String(), `nodeID2: nodeHostname2 Ready/Reachable`) - assert.Contains(t, buf.String(), `nodeID3: nodeHostname3 Ready`) -} - -func TestNodeListFormat(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - *Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())), - *Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()), - }, nil - }, - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - NodeID: "nodeID1", - }, - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{ - NodesFormat: "{{.ID}}: {{.Hostname}} {{.Status}}/{{.ManagerStatus}}", - }) - cmd := newListCommand(cli) - cmd.Flags().Set("format", "{{.Hostname}}: {{.ManagerStatus}}") - assert.NoError(t, cmd.Execute()) - assert.Contains(t, buf.String(), `nodeHostname1: Leader`) - assert.Contains(t, buf.String(), `nodeHostname2: Reachable`) -} diff --git a/cli/command/node/opts.go b/cli/command/node/opts.go deleted file mode 100644 index 0ad365f0c6..0000000000 --- a/cli/command/node/opts.go +++ /dev/null @@ -1,24 +0,0 @@ -package node - -import ( - "github.com/docker/docker/opts" -) - -type nodeOptions struct { - annotations - role string - availability string -} - -type annotations struct { - name string - labels opts.ListOpts -} - -func newNodeOptions() *nodeOptions { - return &nodeOptions{ - annotations: annotations{ - labels: opts.NewListOpts(nil), - }, - } -} diff --git a/cli/command/node/promote.go b/cli/command/node/promote.go deleted file mode 100644 index 94fff6400b..0000000000 --- a/cli/command/node/promote.go +++ /dev/null @@ -1,36 +0,0 @@ -package node - -import ( - "fmt" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -func newPromoteCommand(dockerCli command.Cli) *cobra.Command { - return &cobra.Command{ - Use: "promote NODE [NODE...]", - Short: "Promote one or more nodes to manager in the swarm", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runPromote(dockerCli, args) - }, - } -} - -func runPromote(dockerCli command.Cli, nodes []string) error { - promote := func(node *swarm.Node) error { - if node.Spec.Role == swarm.NodeRoleManager { - fmt.Fprintf(dockerCli.Out(), "Node %s is already a manager.\n", node.ID) - return errNoRoleChange - } - node.Spec.Role = swarm.NodeRoleManager - return nil - } - success := func(nodeID string) { - fmt.Fprintf(dockerCli.Out(), "Node %s promoted to a manager in the swarm.\n", nodeID) - } - return updateNodes(dockerCli, nodes, promote, success) -} diff --git a/cli/command/node/promote_test.go b/cli/command/node/promote_test.go deleted file mode 100644 index ce2fb13dca..0000000000 --- a/cli/command/node/promote_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package node - -import ( - "bytes" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/stretchr/testify/assert" -) - -func TestNodePromoteErrors(t *testing.T) { - testCases := []struct { - args []string - nodeInspectFunc func() (swarm.Node, []byte, error) - nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error - expectedError string - }{ - { - expectedError: "requires at least 1 argument", - }, - { - args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") - }, - expectedError: "error inspecting the node", - }, - { - args: []string{"nodeID"}, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - return errors.Errorf("error updating the node") - }, - expectedError: "error updating the node", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newPromoteCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: tc.nodeInspectFunc, - nodeUpdateFunc: tc.nodeUpdateFunc, - }, buf)) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNodePromoteNoChange(t *testing.T) { - buf := new(bytes.Buffer) - cmd := newPromoteCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager()), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Role != swarm.NodeRoleManager { - return errors.Errorf("expected role manager, got %s", node.Role) - } - return nil - }, - }, buf)) - cmd.SetArgs([]string{"nodeID"}) - assert.NoError(t, cmd.Execute()) -} - -func TestNodePromoteMultipleNode(t *testing.T) { - buf := new(bytes.Buffer) - cmd := newPromoteCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Role != swarm.NodeRoleManager { - return errors.Errorf("expected role manager, got %s", node.Role) - } - return nil - }, - }, buf)) - cmd.SetArgs([]string{"nodeID1", "nodeID2"}) - assert.NoError(t, cmd.Execute()) -} diff --git a/cli/command/node/ps.go b/cli/command/node/ps.go deleted file mode 100644 index 0ab1c0b9f4..0000000000 --- a/cli/command/node/ps.go +++ /dev/null @@ -1,109 +0,0 @@ -package node - -import ( - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/cli/command/idresolver" - "github.com/docker/docker/cli/command/task" - "github.com/docker/docker/opts" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type psOptions struct { - nodeIDs []string - noResolve bool - noTrunc bool - quiet bool - format string - filter opts.FilterOpt -} - -func newPsCommand(dockerCli command.Cli) *cobra.Command { - opts := psOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ps [OPTIONS] [NODE...]", - Short: "List tasks running on one or more nodes, defaults to current node", - Args: cli.RequiresMinArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - opts.nodeIDs = []string{"self"} - - if len(args) != 0 { - opts.nodeIDs = args - } - - return runPs(dockerCli, opts) - }, - } - flags := cmd.Flags() - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") - flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template") - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs") - - return cmd -} - -func runPs(dockerCli command.Cli, opts psOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - var ( - errs []string - tasks []swarm.Task - ) - - for _, nodeID := range opts.nodeIDs { - nodeRef, err := Reference(ctx, client, nodeID) - if err != nil { - errs = append(errs, err.Error()) - continue - } - - node, _, err := client.NodeInspectWithRaw(ctx, nodeRef) - if err != nil { - errs = append(errs, err.Error()) - continue - } - - filter := opts.filter.Value() - filter.Add("node", node.ID) - - nodeTasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter}) - if err != nil { - errs = append(errs, err.Error()) - continue - } - - tasks = append(tasks, nodeTasks...) - } - - format := opts.format - if len(format) == 0 { - if dockerCli.ConfigFile() != nil && len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().TasksFormat - } else { - format = formatter.TableFormatKey - } - } - - if len(errs) == 0 || len(tasks) != 0 { - if err := task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format); err != nil { - errs = append(errs, err.Error()) - } - } - - if len(errs) > 0 { - return errors.Errorf("%s", strings.Join(errs, "\n")) - } - - return nil -} diff --git a/cli/command/node/ps_test.go b/cli/command/node/ps_test.go deleted file mode 100644 index f604628991..0000000000 --- a/cli/command/node/ps_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package node - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestNodePsErrors(t *testing.T) { - testCases := []struct { - args []string - flags map[string]string - infoFunc func() (types.Info, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error) - taskInspectFunc func(taskID string) (swarm.Task, []byte, error) - expectedError string - }{ - { - infoFunc: func() (types.Info, error) { - return types.Info{}, errors.Errorf("error asking for node info") - }, - expectedError: "error asking for node info", - }, - { - args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") - }, - expectedError: "error inspecting the node", - }, - { - args: []string{"nodeID"}, - taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{}, errors.Errorf("error returning the task list") - }, - expectedError: "error returning the task list", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newPsCommand( - test.NewFakeCli(&fakeClient{ - infoFunc: tc.infoFunc, - nodeInspectFunc: tc.nodeInspectFunc, - taskInspectFunc: tc.taskInspectFunc, - taskListFunc: tc.taskListFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - assert.EqualError(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNodePs(t *testing.T) { - testCases := []struct { - name string - args []string - flags map[string]string - infoFunc func() (types.Info, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error) - taskInspectFunc func(taskID string) (swarm.Task, []byte, error) - }{ - { - name: "simple", - args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(), []byte{}, nil - }, - taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{ - *Task(WithStatus(Timestamp(time.Now().Add(-2*time.Hour)), PortStatus([]swarm.PortConfig{ - { - TargetPort: 80, - PublishedPort: 80, - Protocol: "tcp", - }, - }))), - }, nil - }, - }, - { - name: "with-errors", - args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(), []byte{}, nil - }, - taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{ - *Task(TaskID("taskID1"), ServiceID("failure"), - WithStatus(Timestamp(time.Now().Add(-2*time.Hour)), StatusErr("a task error"))), - *Task(TaskID("taskID2"), ServiceID("failure"), - WithStatus(Timestamp(time.Now().Add(-3*time.Hour)), StatusErr("a task error"))), - *Task(TaskID("taskID3"), ServiceID("failure"), - WithStatus(Timestamp(time.Now().Add(-4*time.Hour)), StatusErr("a task error"))), - }, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newPsCommand( - test.NewFakeCli(&fakeClient{ - infoFunc: tc.infoFunc, - nodeInspectFunc: tc.nodeInspectFunc, - taskInspectFunc: tc.taskInspectFunc, - taskListFunc: tc.taskListFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("node-ps.%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} diff --git a/cli/command/node/remove.go b/cli/command/node/remove.go deleted file mode 100644 index bd429ee45f..0000000000 --- a/cli/command/node/remove.go +++ /dev/null @@ -1,57 +0,0 @@ -package node - -import ( - "fmt" - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type removeOptions struct { - force bool -} - -func newRemoveCommand(dockerCli command.Cli) *cobra.Command { - opts := removeOptions{} - - cmd := &cobra.Command{ - Use: "rm [OPTIONS] NODE [NODE...]", - Aliases: []string{"remove"}, - Short: "Remove one or more nodes from the swarm", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(dockerCli, args, opts) - }, - } - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Force remove a node from the swarm") - return cmd -} - -func runRemove(dockerCli command.Cli, args []string, opts removeOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - var errs []string - - for _, nodeID := range args { - err := client.NodeRemove(ctx, nodeID, types.NodeRemoveOptions{Force: opts.force}) - if err != nil { - errs = append(errs, err.Error()) - continue - } - fmt.Fprintf(dockerCli.Out(), "%s\n", nodeID) - } - - if len(errs) > 0 { - return errors.Errorf("%s", strings.Join(errs, "\n")) - } - - return nil -} diff --git a/cli/command/node/remove_test.go b/cli/command/node/remove_test.go deleted file mode 100644 index b53431dfa7..0000000000 --- a/cli/command/node/remove_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package node - -import ( - "bytes" - "io/ioutil" - "testing" - - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestNodeRemoveErrors(t *testing.T) { - testCases := []struct { - args []string - nodeRemoveFunc func() error - expectedError string - }{ - { - expectedError: "requires at least 1 argument", - }, - { - args: []string{"nodeID"}, - nodeRemoveFunc: func() error { - return errors.Errorf("error removing the node") - }, - expectedError: "error removing the node", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newRemoveCommand( - test.NewFakeCli(&fakeClient{ - nodeRemoveFunc: tc.nodeRemoveFunc, - }, buf)) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNodeRemoveMultiple(t *testing.T) { - buf := new(bytes.Buffer) - cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}, buf)) - cmd.SetArgs([]string{"nodeID1", "nodeID2"}) - assert.NoError(t, cmd.Execute()) -} diff --git a/cli/command/node/testdata/node-inspect-pretty.manager-leader.golden b/cli/command/node/testdata/node-inspect-pretty.manager-leader.golden deleted file mode 100644 index 461fc46ea2..0000000000 --- a/cli/command/node/testdata/node-inspect-pretty.manager-leader.golden +++ /dev/null @@ -1,25 +0,0 @@ -ID: nodeID -Name: defaultNodeName -Hostname: defaultNodeHostname -Joined at: 2009-11-10 23:00:00 +0000 utc -Status: - State: Ready - Availability: Active - Address: 127.0.0.1 -Manager Status: - Address: 127.0.0.1 - Raft Status: Reachable - Leader: Yes -Platform: - Operating System: linux - Architecture: x86_64 -Resources: - CPUs: 0 - Memory: 20 MiB -Plugins: - Network: bridge, overlay - Volume: local -Engine Version: 1.13.0 -Engine Labels: - - engine = label - diff --git a/cli/command/node/testdata/node-inspect-pretty.manager.golden b/cli/command/node/testdata/node-inspect-pretty.manager.golden deleted file mode 100644 index 2c660188d5..0000000000 --- a/cli/command/node/testdata/node-inspect-pretty.manager.golden +++ /dev/null @@ -1,25 +0,0 @@ -ID: nodeID -Name: defaultNodeName -Hostname: defaultNodeHostname -Joined at: 2009-11-10 23:00:00 +0000 utc -Status: - State: Ready - Availability: Active - Address: 127.0.0.1 -Manager Status: - Address: 127.0.0.1 - Raft Status: Reachable - Leader: No -Platform: - Operating System: linux - Architecture: x86_64 -Resources: - CPUs: 0 - Memory: 20 MiB -Plugins: - Network: bridge, overlay - Volume: local -Engine Version: 1.13.0 -Engine Labels: - - engine = label - diff --git a/cli/command/node/testdata/node-inspect-pretty.simple.golden b/cli/command/node/testdata/node-inspect-pretty.simple.golden deleted file mode 100644 index e63bc12596..0000000000 --- a/cli/command/node/testdata/node-inspect-pretty.simple.golden +++ /dev/null @@ -1,23 +0,0 @@ -ID: nodeID -Name: defaultNodeName -Labels: - - lbl1 = value1 -Hostname: defaultNodeHostname -Joined at: 2009-11-10 23:00:00 +0000 utc -Status: - State: Ready - Availability: Active - Address: 127.0.0.1 -Platform: - Operating System: linux - Architecture: x86_64 -Resources: - CPUs: 0 - Memory: 20 MiB -Plugins: - Network: bridge, overlay - Volume: local -Engine Version: 1.13.0 -Engine Labels: - - engine = label - diff --git a/cli/command/node/testdata/node-ps.simple.golden b/cli/command/node/testdata/node-ps.simple.golden deleted file mode 100644 index f9555d8792..0000000000 --- a/cli/command/node/testdata/node-ps.simple.golden +++ /dev/null @@ -1,2 +0,0 @@ -ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS -taskID rl02d5gwz6chzu7il5fhtb8be.1 myimage:mytag defaultNodeName Ready Ready 2 hours ago *:80->80/tcp diff --git a/cli/command/node/testdata/node-ps.with-errors.golden b/cli/command/node/testdata/node-ps.with-errors.golden deleted file mode 100644 index 273b30fa11..0000000000 --- a/cli/command/node/testdata/node-ps.with-errors.golden +++ /dev/null @@ -1,4 +0,0 @@ -ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS -taskID1 failure.1 myimage:mytag defaultNodeName Ready Ready 2 hours ago "a task error" -taskID2 \_ failure.1 myimage:mytag defaultNodeName Ready Ready 3 hours ago "a task error" -taskID3 \_ failure.1 myimage:mytag defaultNodeName Ready Ready 4 hours ago "a task error" diff --git a/cli/command/node/update.go b/cli/command/node/update.go deleted file mode 100644 index 82668595a7..0000000000 --- a/cli/command/node/update.go +++ /dev/null @@ -1,121 +0,0 @@ -package node - -import ( - "fmt" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "golang.org/x/net/context" -) - -var ( - errNoRoleChange = errors.New("role was already set to the requested value") -) - -func newUpdateCommand(dockerCli command.Cli) *cobra.Command { - nodeOpts := newNodeOptions() - - cmd := &cobra.Command{ - Use: "update [OPTIONS] NODE", - Short: "Update a node", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runUpdate(dockerCli, cmd.Flags(), args[0]) - }, - } - - flags := cmd.Flags() - flags.StringVar(&nodeOpts.role, flagRole, "", `Role of the node ("worker"|"manager")`) - flags.StringVar(&nodeOpts.availability, flagAvailability, "", `Availability of the node ("active"|"pause"|"drain")`) - flags.Var(&nodeOpts.annotations.labels, flagLabelAdd, "Add or update a node label (key=value)") - labelKeys := opts.NewListOpts(nil) - flags.Var(&labelKeys, flagLabelRemove, "Remove a node label if exists") - return cmd -} - -func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, nodeID string) error { - success := func(_ string) { - fmt.Fprintln(dockerCli.Out(), nodeID) - } - return updateNodes(dockerCli, []string{nodeID}, mergeNodeUpdate(flags), success) -} - -func updateNodes(dockerCli command.Cli, nodes []string, mergeNode func(node *swarm.Node) error, success func(nodeID string)) error { - client := dockerCli.Client() - ctx := context.Background() - - for _, nodeID := range nodes { - node, _, err := client.NodeInspectWithRaw(ctx, nodeID) - if err != nil { - return err - } - - err = mergeNode(&node) - if err != nil { - if err == errNoRoleChange { - continue - } - return err - } - err = client.NodeUpdate(ctx, node.ID, node.Version, node.Spec) - if err != nil { - return err - } - success(nodeID) - } - return nil -} - -func mergeNodeUpdate(flags *pflag.FlagSet) func(*swarm.Node) error { - return func(node *swarm.Node) error { - spec := &node.Spec - - if flags.Changed(flagRole) { - str, err := flags.GetString(flagRole) - if err != nil { - return err - } - spec.Role = swarm.NodeRole(str) - } - if flags.Changed(flagAvailability) { - str, err := flags.GetString(flagAvailability) - if err != nil { - return err - } - spec.Availability = swarm.NodeAvailability(str) - } - if spec.Annotations.Labels == nil { - spec.Annotations.Labels = make(map[string]string) - } - if flags.Changed(flagLabelAdd) { - labels := flags.Lookup(flagLabelAdd).Value.(*opts.ListOpts).GetAll() - for k, v := range runconfigopts.ConvertKVStringsToMap(labels) { - spec.Annotations.Labels[k] = v - } - } - if flags.Changed(flagLabelRemove) { - keys := flags.Lookup(flagLabelRemove).Value.(*opts.ListOpts).GetAll() - for _, k := range keys { - // if a key doesn't exist, fail the command explicitly - if _, exists := spec.Annotations.Labels[k]; !exists { - return errors.Errorf("key %s doesn't exist in node's labels", k) - } - delete(spec.Annotations.Labels, k) - } - } - return nil - } -} - -const ( - flagRole = "role" - flagAvailability = "availability" - flagLabelAdd = "label-add" - flagLabelRemove = "label-rm" -) diff --git a/cli/command/node/update_test.go b/cli/command/node/update_test.go deleted file mode 100644 index a5e2d20e9a..0000000000 --- a/cli/command/node/update_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package node - -import ( - "bytes" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/stretchr/testify/assert" -) - -func TestNodeUpdateErrors(t *testing.T) { - testCases := []struct { - args []string - flags map[string]string - nodeInspectFunc func() (swarm.Node, []byte, error) - nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error - expectedError string - }{ - { - expectedError: "requires exactly 1 argument", - }, - { - args: []string{"node1", "node2"}, - expectedError: "requires exactly 1 argument", - }, - { - args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") - }, - expectedError: "error inspecting the node", - }, - { - args: []string{"nodeID"}, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - return errors.Errorf("error updating the node") - }, - expectedError: "error updating the node", - }, - { - args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(NodeLabels(map[string]string{ - "key": "value", - })), []byte{}, nil - }, - flags: map[string]string{ - "label-rm": "notpresent", - }, - expectedError: "key notpresent doesn't exist in node's labels", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newUpdateCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: tc.nodeInspectFunc, - nodeUpdateFunc: tc.nodeUpdateFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestNodeUpdate(t *testing.T) { - testCases := []struct { - args []string - flags map[string]string - nodeInspectFunc func() (swarm.Node, []byte, error) - nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error - }{ - { - args: []string{"nodeID"}, - flags: map[string]string{ - "role": "manager", - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Role != swarm.NodeRoleManager { - return errors.Errorf("expected role manager, got %s", node.Role) - } - return nil - }, - }, - { - args: []string{"nodeID"}, - flags: map[string]string{ - "availability": "drain", - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Availability != swarm.NodeAvailabilityDrain { - return errors.Errorf("expected drain availability, got %s", node.Availability) - } - return nil - }, - }, - { - args: []string{"nodeID"}, - flags: map[string]string{ - "label-add": "lbl", - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if _, present := node.Annotations.Labels["lbl"]; !present { - return errors.Errorf("expected 'lbl' label, got %v", node.Annotations.Labels) - } - return nil - }, - }, - { - args: []string{"nodeID"}, - flags: map[string]string{ - "label-add": "key=value", - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if value, present := node.Annotations.Labels["key"]; !present || value != "value" { - return errors.Errorf("expected 'key' label to be 'value', got %v", node.Annotations.Labels) - } - return nil - }, - }, - { - args: []string{"nodeID"}, - flags: map[string]string{ - "label-rm": "key", - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(NodeLabels(map[string]string{ - "key": "value", - })), []byte{}, nil - }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if len(node.Annotations.Labels) > 0 { - return errors.Errorf("expected no labels, got %v", node.Annotations.Labels) - } - return nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newUpdateCommand( - test.NewFakeCli(&fakeClient{ - nodeInspectFunc: tc.nodeInspectFunc, - nodeUpdateFunc: tc.nodeUpdateFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - assert.NoError(t, cmd.Execute()) - } -} diff --git a/cli/command/out.go b/cli/command/out.go deleted file mode 100644 index 27b44c235d..0000000000 --- a/cli/command/out.go +++ /dev/null @@ -1,50 +0,0 @@ -package command - -import ( - "io" - "os" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/pkg/term" -) - -// OutStream is an output stream used by the DockerCli to write normal program -// output. -type OutStream struct { - CommonStream - out io.Writer -} - -func (o *OutStream) Write(p []byte) (int, error) { - return o.out.Write(p) -} - -// SetRawTerminal sets raw mode on the input terminal -func (o *OutStream) SetRawTerminal() (err error) { - if os.Getenv("NORAW") != "" || !o.CommonStream.isTerminal { - return nil - } - o.CommonStream.state, err = term.SetRawTerminalOutput(o.CommonStream.fd) - return err -} - -// GetTtySize returns the height and width in characters of the tty -func (o *OutStream) GetTtySize() (uint, uint) { - if !o.isTerminal { - return 0, 0 - } - ws, err := term.GetWinsize(o.fd) - if err != nil { - logrus.Debugf("Error getting size: %s", err) - if ws == nil { - return 0, 0 - } - } - return uint(ws.Height), uint(ws.Width) -} - -// NewOutStream returns a new OutStream object from a Writer -func NewOutStream(out io.Writer) *OutStream { - fd, isTerminal := term.GetFdInfo(out) - return &OutStream{CommonStream: CommonStream{fd: fd, isTerminal: isTerminal}, out: out} -} diff --git a/cli/command/plugin/cmd.go b/cli/command/plugin/cmd.go deleted file mode 100644 index 33046d2cb8..0000000000 --- a/cli/command/plugin/cmd.go +++ /dev/null @@ -1,32 +0,0 @@ -package plugin - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -// NewPluginCommand returns a cobra command for `plugin` subcommands -func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "plugin", - Short: "Manage plugins", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - Tags: map[string]string{"version": "1.25"}, - } - - cmd.AddCommand( - newDisableCommand(dockerCli), - newEnableCommand(dockerCli), - newInspectCommand(dockerCli), - newInstallCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - newSetCommand(dockerCli), - newPushCommand(dockerCli), - newCreateCommand(dockerCli), - newUpgradeCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/plugin/create.go b/cli/command/plugin/create.go deleted file mode 100644 index b51f1933db..0000000000 --- a/cli/command/plugin/create.go +++ /dev/null @@ -1,128 +0,0 @@ -package plugin - -import ( - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - - "github.com/Sirupsen/logrus" - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/archive" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -// validateTag checks if the given repoName can be resolved. -func validateTag(rawRepo string) error { - _, err := reference.ParseNormalizedNamed(rawRepo) - - return err -} - -// validateConfig ensures that a valid config.json is available in the given path -func validateConfig(path string) error { - dt, err := os.Open(filepath.Join(path, "config.json")) - if err != nil { - return err - } - - m := types.PluginConfig{} - err = json.NewDecoder(dt).Decode(&m) - dt.Close() - - return err -} - -// validateContextDir validates the given dir and returns abs path on success. -func validateContextDir(contextDir string) (string, error) { - absContextDir, err := filepath.Abs(contextDir) - if err != nil { - return "", err - } - stat, err := os.Lstat(absContextDir) - if err != nil { - return "", err - } - - if !stat.IsDir() { - return "", errors.Errorf("context must be a directory") - } - - return absContextDir, nil -} - -type pluginCreateOptions struct { - repoName string - context string - compress bool -} - -func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { - options := pluginCreateOptions{} - - cmd := &cobra.Command{ - Use: "create [OPTIONS] PLUGIN PLUGIN-DATA-DIR", - Short: "Create a plugin from a rootfs and configuration. Plugin data directory must contain config.json and rootfs directory.", - Args: cli.RequiresMinArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - options.repoName = args[0] - options.context = args[1] - return runCreate(dockerCli, options) - }, - } - - flags := cmd.Flags() - - flags.BoolVar(&options.compress, "compress", false, "Compress the context using gzip") - - return cmd -} - -func runCreate(dockerCli *command.DockerCli, options pluginCreateOptions) error { - var ( - createCtx io.ReadCloser - err error - ) - - if err := validateTag(options.repoName); err != nil { - return err - } - - absContextDir, err := validateContextDir(options.context) - if err != nil { - return err - } - - if err := validateConfig(options.context); err != nil { - return err - } - - compression := archive.Uncompressed - if options.compress { - logrus.Debugf("compression enabled") - compression = archive.Gzip - } - - createCtx, err = archive.TarWithOptions(absContextDir, &archive.TarOptions{ - Compression: compression, - }) - - if err != nil { - return err - } - - ctx := context.Background() - - createOptions := types.PluginCreateOptions{RepoName: options.repoName} - if err = dockerCli.Client().PluginCreate(ctx, createCtx, createOptions); err != nil { - return err - } - fmt.Fprintln(dockerCli.Out(), options.repoName) - return nil -} diff --git a/cli/command/plugin/disable.go b/cli/command/plugin/disable.go deleted file mode 100644 index 07b0ec2288..0000000000 --- a/cli/command/plugin/disable.go +++ /dev/null @@ -1,36 +0,0 @@ -package plugin - -import ( - "fmt" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -func newDisableCommand(dockerCli *command.DockerCli) *cobra.Command { - var force bool - - cmd := &cobra.Command{ - Use: "disable [OPTIONS] PLUGIN", - Short: "Disable a plugin", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runDisable(dockerCli, args[0], force) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&force, "force", "f", false, "Force the disable of an active plugin") - return cmd -} - -func runDisable(dockerCli *command.DockerCli, name string, force bool) error { - if err := dockerCli.Client().PluginDisable(context.Background(), name, types.PluginDisableOptions{Force: force}); err != nil { - return err - } - fmt.Fprintln(dockerCli.Out(), name) - return nil -} diff --git a/cli/command/plugin/enable.go b/cli/command/plugin/enable.go deleted file mode 100644 index b1ca48f8f1..0000000000 --- a/cli/command/plugin/enable.go +++ /dev/null @@ -1,48 +0,0 @@ -package plugin - -import ( - "fmt" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type enableOpts struct { - timeout int - name string -} - -func newEnableCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts enableOpts - - cmd := &cobra.Command{ - Use: "enable [OPTIONS] PLUGIN", - Short: "Enable a plugin", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.name = args[0] - return runEnable(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.IntVar(&opts.timeout, "timeout", 0, "HTTP client timeout (in seconds)") - return cmd -} - -func runEnable(dockerCli *command.DockerCli, opts *enableOpts) error { - name := opts.name - if opts.timeout < 0 { - return errors.Errorf("negative timeout %d is invalid", opts.timeout) - } - - if err := dockerCli.Client().PluginEnable(context.Background(), name, types.PluginEnableOptions{Timeout: opts.timeout}); err != nil { - return err - } - fmt.Fprintln(dockerCli.Out(), name) - return nil -} diff --git a/cli/command/plugin/inspect.go b/cli/command/plugin/inspect.go deleted file mode 100644 index c2c7a0d6bc..0000000000 --- a/cli/command/plugin/inspect.go +++ /dev/null @@ -1,42 +0,0 @@ -package plugin - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type inspectOptions struct { - pluginNames []string - format string -} - -func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts inspectOptions - - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] PLUGIN [PLUGIN...]", - Short: "Display detailed information on one or more plugins", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.pluginNames = args - return runInspect(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - return cmd -} - -func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { - client := dockerCli.Client() - ctx := context.Background() - getRef := func(ref string) (interface{}, []byte, error) { - return client.PluginInspectWithRaw(ctx, ref) - } - - return inspect.Inspect(dockerCli.Out(), opts.pluginNames, opts.format, getRef) -} diff --git a/cli/command/plugin/install.go b/cli/command/plugin/install.go deleted file mode 100644 index 18b3fa3739..0000000000 --- a/cli/command/plugin/install.go +++ /dev/null @@ -1,168 +0,0 @@ -package plugin - -import ( - "fmt" - "strings" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/image" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/registry" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "golang.org/x/net/context" -) - -type pluginOptions struct { - remote string - localName string - grantPerms bool - disable bool - args []string - skipRemoteCheck bool -} - -func loadPullFlags(opts *pluginOptions, flags *pflag.FlagSet) { - flags.BoolVar(&opts.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin") - command.AddTrustVerificationFlags(flags) -} - -func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command { - var options pluginOptions - cmd := &cobra.Command{ - Use: "install [OPTIONS] PLUGIN [KEY=VALUE...]", - Short: "Install a plugin", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - options.remote = args[0] - if len(args) > 1 { - options.args = args[1:] - } - return runInstall(dockerCli, options) - }, - } - - flags := cmd.Flags() - loadPullFlags(&options, flags) - flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install") - flags.StringVar(&options.localName, "alias", "", "Local name for plugin") - return cmd -} - -type pluginRegistryService struct { - registry.Service -} - -func (s pluginRegistryService) ResolveRepository(name reference.Named) (repoInfo *registry.RepositoryInfo, err error) { - repoInfo, err = s.Service.ResolveRepository(name) - if repoInfo != nil { - repoInfo.Class = "plugin" - } - return -} - -func newRegistryService() registry.Service { - return pluginRegistryService{ - Service: registry.NewService(registry.ServiceOptions{V2Only: true}), - } -} - -func buildPullConfig(ctx context.Context, dockerCli *command.DockerCli, opts pluginOptions, cmdName string) (types.PluginInstallOptions, error) { - // Names with both tag and digest will be treated by the daemon - // as a pull by digest with a local name for the tag - // (if no local name is provided). - ref, err := reference.ParseNormalizedNamed(opts.remote) - if err != nil { - return types.PluginInstallOptions{}, err - } - - repoInfo, err := registry.ParseRepositoryInfo(ref) - if err != nil { - return types.PluginInstallOptions{}, err - } - - remote := ref.String() - - _, isCanonical := ref.(reference.Canonical) - if command.IsTrusted() && !isCanonical { - ref = reference.TagNameOnly(ref) - nt, ok := ref.(reference.NamedTagged) - if !ok { - return types.PluginInstallOptions{}, errors.Errorf("invalid name: %s", ref.String()) - } - - ctx := context.Background() - trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService()) - if err != nil { - return types.PluginInstallOptions{}, err - } - remote = reference.FamiliarString(trusted) - } - - authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) - - encodedAuth, err := command.EncodeAuthToBase64(authConfig) - if err != nil { - return types.PluginInstallOptions{}, err - } - registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, cmdName) - - options := types.PluginInstallOptions{ - RegistryAuth: encodedAuth, - RemoteRef: remote, - Disabled: opts.disable, - AcceptAllPermissions: opts.grantPerms, - AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.remote), - // TODO: Rename PrivilegeFunc, it has nothing to do with privileges - PrivilegeFunc: registryAuthFunc, - Args: opts.args, - } - return options, nil -} - -func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { - var localName string - if opts.localName != "" { - aref, err := reference.ParseNormalizedNamed(opts.localName) - if err != nil { - return err - } - if _, ok := aref.(reference.Canonical); ok { - return errors.Errorf("invalid name: %s", opts.localName) - } - localName = reference.FamiliarString(reference.TagNameOnly(aref)) - } - - ctx := context.Background() - options, err := buildPullConfig(ctx, dockerCli, opts, "plugin install") - if err != nil { - return err - } - responseBody, err := dockerCli.Client().PluginInstall(ctx, localName, options) - if err != nil { - if strings.Contains(err.Error(), "(image) when fetching") { - return errors.New(err.Error() + " - Use `docker image pull`") - } - return err - } - defer responseBody.Close() - if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil { - return err - } - fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.remote) // todo: return proper values from the API for this result - return nil -} - -func acceptPrivileges(dockerCli *command.DockerCli, name string) func(privileges types.PluginPrivileges) (bool, error) { - return func(privileges types.PluginPrivileges) (bool, error) { - fmt.Fprintf(dockerCli.Out(), "Plugin %q is requesting the following privileges:\n", name) - for _, privilege := range privileges { - fmt.Fprintf(dockerCli.Out(), " - %s: %v\n", privilege.Name, privilege.Value) - } - return command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), "Do you grant the above permissions?"), nil - } -} diff --git a/cli/command/plugin/list.go b/cli/command/plugin/list.go deleted file mode 100644 index a1b231f570..0000000000 --- a/cli/command/plugin/list.go +++ /dev/null @@ -1,63 +0,0 @@ -package plugin - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type listOptions struct { - quiet bool - noTrunc bool - format string - filter opts.FilterOpt -} - -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := listOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ls [OPTIONS]", - Short: "List plugins", - Aliases: []string{"list"}, - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display plugin IDs") - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") - flags.StringVar(&opts.format, "format", "", "Pretty-print plugins using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Provide filter values (e.g. 'enabled=true')") - - return cmd -} - -func runList(dockerCli *command.DockerCli, opts listOptions) error { - plugins, err := dockerCli.Client().PluginList(context.Background(), opts.filter.Value()) - if err != nil { - return err - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().PluginsFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().PluginsFormat - } else { - format = formatter.TableFormatKey - } - } - - pluginsCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewPluginFormat(format, opts.quiet), - Trunc: !opts.noTrunc, - } - return formatter.PluginWrite(pluginsCtx, plugins) -} diff --git a/cli/command/plugin/push.go b/cli/command/plugin/push.go deleted file mode 100644 index de4f95cce8..0000000000 --- a/cli/command/plugin/push.go +++ /dev/null @@ -1,69 +0,0 @@ -package plugin - -import ( - "golang.org/x/net/context" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/image" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/registry" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -func newPushCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "push [OPTIONS] PLUGIN[:TAG]", - Short: "Push a plugin to a registry", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runPush(dockerCli, args[0]) - }, - } - - flags := cmd.Flags() - - command.AddTrustSigningFlags(flags) - - return cmd -} - -func runPush(dockerCli *command.DockerCli, name string) error { - named, err := reference.ParseNormalizedNamed(name) - if err != nil { - return err - } - if _, ok := named.(reference.Canonical); ok { - return errors.Errorf("invalid name: %s", name) - } - - named = reference.TagNameOnly(named) - - ctx := context.Background() - - repoInfo, err := registry.ParseRepositoryInfo(named) - if err != nil { - return err - } - authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) - - encodedAuth, err := command.EncodeAuthToBase64(authConfig) - if err != nil { - return err - } - - responseBody, err := dockerCli.Client().PluginPush(ctx, reference.FamiliarString(named), encodedAuth) - if err != nil { - return err - } - defer responseBody.Close() - - if command.IsTrusted() { - repoInfo.Class = "plugin" - return image.PushTrustedReference(dockerCli, repoInfo, named, authConfig, responseBody) - } - - return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil) -} diff --git a/cli/command/plugin/remove.go b/cli/command/plugin/remove.go deleted file mode 100644 index 9f3aba9a01..0000000000 --- a/cli/command/plugin/remove.go +++ /dev/null @@ -1,55 +0,0 @@ -package plugin - -import ( - "fmt" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type rmOptions struct { - force bool - - plugins []string -} - -func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts rmOptions - - cmd := &cobra.Command{ - Use: "rm [OPTIONS] PLUGIN [PLUGIN...]", - Short: "Remove one or more plugins", - Aliases: []string{"remove"}, - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.plugins = args - return runRemove(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of an active plugin") - return cmd -} - -func runRemove(dockerCli *command.DockerCli, opts *rmOptions) error { - ctx := context.Background() - - var errs cli.Errors - for _, name := range opts.plugins { - // TODO: pass names to api instead of making multiple api calls - if err := dockerCli.Client().PluginRemove(ctx, name, types.PluginRemoveOptions{Force: opts.force}); err != nil { - errs = append(errs, err) - continue - } - fmt.Fprintln(dockerCli.Out(), name) - } - // Do not simplify to `return errs` because even if errs == nil, it is not a nil-error interface value. - if errs != nil { - return errs - } - return nil -} diff --git a/cli/command/plugin/set.go b/cli/command/plugin/set.go deleted file mode 100644 index 52b09fb500..0000000000 --- a/cli/command/plugin/set.go +++ /dev/null @@ -1,22 +0,0 @@ -package plugin - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -func newSetCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "set PLUGIN KEY=VALUE [KEY=VALUE...]", - Short: "Change settings for a plugin", - Args: cli.RequiresMinArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - return dockerCli.Client().PluginSet(context.Background(), args[0], args[1:]) - }, - } - - return cmd -} diff --git a/cli/command/plugin/upgrade.go b/cli/command/plugin/upgrade.go deleted file mode 100644 index cbcbe17ece..0000000000 --- a/cli/command/plugin/upgrade.go +++ /dev/null @@ -1,90 +0,0 @@ -package plugin - -import ( - "context" - "fmt" - "strings" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -func newUpgradeCommand(dockerCli *command.DockerCli) *cobra.Command { - var options pluginOptions - cmd := &cobra.Command{ - Use: "upgrade [OPTIONS] PLUGIN [REMOTE]", - Short: "Upgrade an existing plugin", - Args: cli.RequiresRangeArgs(1, 2), - RunE: func(cmd *cobra.Command, args []string) error { - options.localName = args[0] - if len(args) == 2 { - options.remote = args[1] - } - return runUpgrade(dockerCli, options) - }, - Tags: map[string]string{"version": "1.26"}, - } - - flags := cmd.Flags() - loadPullFlags(&options, flags) - flags.BoolVar(&options.skipRemoteCheck, "skip-remote-check", false, "Do not check if specified remote plugin matches existing plugin image") - return cmd -} - -func runUpgrade(dockerCli *command.DockerCli, opts pluginOptions) error { - ctx := context.Background() - p, _, err := dockerCli.Client().PluginInspectWithRaw(ctx, opts.localName) - if err != nil { - return errors.Errorf("error reading plugin data: %v", err) - } - - if p.Enabled { - return errors.Errorf("the plugin must be disabled before upgrading") - } - - opts.localName = p.Name - if opts.remote == "" { - opts.remote = p.PluginReference - } - remote, err := reference.ParseNormalizedNamed(opts.remote) - if err != nil { - return errors.Wrap(err, "error parsing remote upgrade image reference") - } - remote = reference.TagNameOnly(remote) - - old, err := reference.ParseNormalizedNamed(p.PluginReference) - if err != nil { - return errors.Wrap(err, "error parsing current image reference") - } - old = reference.TagNameOnly(old) - - fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, reference.FamiliarString(old), reference.FamiliarString(remote)) - if !opts.skipRemoteCheck && remote.String() != old.String() { - if !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), "Plugin images do not match, are you sure?") { - return errors.New("canceling upgrade request") - } - } - - options, err := buildPullConfig(ctx, dockerCli, opts, "plugin upgrade") - if err != nil { - return err - } - - responseBody, err := dockerCli.Client().PluginUpgrade(ctx, opts.localName, options) - if err != nil { - if strings.Contains(err.Error(), "target is image") { - return errors.New(err.Error() + " - Use `docker image pull`") - } - return err - } - defer responseBody.Close() - if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil { - return err - } - fmt.Fprintf(dockerCli.Out(), "Upgraded plugin %s to %s\n", opts.localName, opts.remote) // todo: return proper values from the API for this result - return nil -} diff --git a/cli/command/prune/prune.go b/cli/command/prune/prune.go deleted file mode 100644 index 26153ed7c1..0000000000 --- a/cli/command/prune/prune.go +++ /dev/null @@ -1,51 +0,0 @@ -package prune - -import ( - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/container" - "github.com/docker/docker/cli/command/image" - "github.com/docker/docker/cli/command/network" - "github.com/docker/docker/cli/command/volume" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" -) - -// NewContainerPruneCommand returns a cobra prune command for containers -func NewContainerPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - return container.NewPruneCommand(dockerCli) -} - -// NewVolumePruneCommand returns a cobra prune command for volumes -func NewVolumePruneCommand(dockerCli *command.DockerCli) *cobra.Command { - return volume.NewPruneCommand(dockerCli) -} - -// NewImagePruneCommand returns a cobra prune command for images -func NewImagePruneCommand(dockerCli *command.DockerCli) *cobra.Command { - return image.NewPruneCommand(dockerCli) -} - -// NewNetworkPruneCommand returns a cobra prune command for Networks -func NewNetworkPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - return network.NewPruneCommand(dockerCli) -} - -// RunContainerPrune executes a prune command for containers -func RunContainerPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { - return container.RunPrune(dockerCli, filter) -} - -// RunVolumePrune executes a prune command for volumes -func RunVolumePrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { - return volume.RunPrune(dockerCli, filter) -} - -// RunImagePrune executes a prune command for images -func RunImagePrune(dockerCli *command.DockerCli, all bool, filter opts.FilterOpt) (uint64, string, error) { - return image.RunPrune(dockerCli, all, filter) -} - -// RunNetworkPrune executes a prune command for networks -func RunNetworkPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { - return network.RunPrune(dockerCli, filter) -} diff --git a/cli/command/registry.go b/cli/command/registry.go deleted file mode 100644 index 884fa6ec40..0000000000 --- a/cli/command/registry.go +++ /dev/null @@ -1,187 +0,0 @@ -package command - -import ( - "bufio" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "os" - "runtime" - "strings" - - "golang.org/x/net/context" - - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/pkg/term" - "github.com/docker/docker/registry" - "github.com/pkg/errors" -) - -// ElectAuthServer returns the default registry to use (by asking the daemon) -func ElectAuthServer(ctx context.Context, cli Cli) string { - // The daemon `/info` endpoint informs us of the default registry being - // used. This is essential in cross-platforms environment, where for - // example a Linux client might be interacting with a Windows daemon, hence - // the default registry URL might be Windows specific. - serverAddress := registry.IndexServer - if info, err := cli.Client().Info(ctx); err != nil { - fmt.Fprintf(cli.Out(), "Warning: failed to get default registry endpoint from daemon (%v). Using system default: %s\n", err, serverAddress) - } else { - serverAddress = info.IndexServerAddress - } - return serverAddress -} - -// EncodeAuthToBase64 serializes the auth configuration as JSON base64 payload -func EncodeAuthToBase64(authConfig types.AuthConfig) (string, error) { - buf, err := json.Marshal(authConfig) - if err != nil { - return "", err - } - return base64.URLEncoding.EncodeToString(buf), nil -} - -// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info -// for the given command. -func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc { - return func() (string, error) { - fmt.Fprintf(cli.Out(), "\nPlease login prior to %s:\n", cmdName) - indexServer := registry.GetAuthConfigKey(index) - isDefaultRegistry := indexServer == ElectAuthServer(context.Background(), cli) - authConfig, err := ConfigureAuth(cli, "", "", indexServer, isDefaultRegistry) - if err != nil { - return "", err - } - return EncodeAuthToBase64(authConfig) - } -} - -// ResolveAuthConfig is like registry.ResolveAuthConfig, but if using the -// default index, it uses the default index name for the daemon's platform, -// not the client's platform. -func ResolveAuthConfig(ctx context.Context, cli Cli, index *registrytypes.IndexInfo) types.AuthConfig { - configKey := index.Name - if index.Official { - configKey = ElectAuthServer(ctx, cli) - } - - a, _ := cli.CredentialsStore(configKey).Get(configKey) - return a -} - -// ConfigureAuth returns an AuthConfig from the specified user, password and server. -func ConfigureAuth(cli Cli, flUser, flPassword, serverAddress string, isDefaultRegistry bool) (types.AuthConfig, error) { - // On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210 - if runtime.GOOS == "windows" { - cli.SetIn(NewInStream(os.Stdin)) - } - - if !isDefaultRegistry { - serverAddress = registry.ConvertToHostname(serverAddress) - } - - authconfig, err := cli.CredentialsStore(serverAddress).Get(serverAddress) - if err != nil { - return authconfig, err - } - - // Some links documenting this: - // - https://code.google.com/archive/p/mintty/issues/56 - // - https://github.com/docker/docker/issues/15272 - // - https://mintty.github.io/ (compatibility) - // Linux will hit this if you attempt `cat | docker login`, and Windows - // will hit this if you attempt docker login from mintty where stdin - // is a pipe, not a character based console. - if flPassword == "" && !cli.In().IsTerminal() { - return authconfig, errors.Errorf("Error: Cannot perform an interactive login from a non TTY device") - } - - authconfig.Username = strings.TrimSpace(authconfig.Username) - - if flUser = strings.TrimSpace(flUser); flUser == "" { - if isDefaultRegistry { - // if this is a default registry (docker hub), then display the following message. - fmt.Fprintln(cli.Out(), "Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.") - } - promptWithDefault(cli.Out(), "Username", authconfig.Username) - flUser = readInput(cli.In(), cli.Out()) - flUser = strings.TrimSpace(flUser) - if flUser == "" { - flUser = authconfig.Username - } - } - if flUser == "" { - return authconfig, errors.Errorf("Error: Non-null Username Required") - } - if flPassword == "" { - oldState, err := term.SaveState(cli.In().FD()) - if err != nil { - return authconfig, err - } - fmt.Fprintf(cli.Out(), "Password: ") - term.DisableEcho(cli.In().FD(), oldState) - - flPassword = readInput(cli.In(), cli.Out()) - fmt.Fprint(cli.Out(), "\n") - - term.RestoreTerminal(cli.In().FD(), oldState) - if flPassword == "" { - return authconfig, errors.Errorf("Error: Password Required") - } - } - - authconfig.Username = flUser - authconfig.Password = flPassword - authconfig.ServerAddress = serverAddress - authconfig.IdentityToken = "" - - return authconfig, nil -} - -func readInput(in io.Reader, out io.Writer) string { - reader := bufio.NewReader(in) - line, _, err := reader.ReadLine() - if err != nil { - fmt.Fprintln(out, err.Error()) - os.Exit(1) - } - return string(line) -} - -func promptWithDefault(out io.Writer, prompt string, configDefault string) { - if configDefault == "" { - fmt.Fprintf(out, "%s: ", prompt) - } else { - fmt.Fprintf(out, "%s (%s): ", prompt, configDefault) - } -} - -// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete image -func RetrieveAuthTokenFromImage(ctx context.Context, cli Cli, image string) (string, error) { - // Retrieve encoded auth token from the image reference - authConfig, err := resolveAuthConfigFromImage(ctx, cli, image) - if err != nil { - return "", err - } - encodedAuth, err := EncodeAuthToBase64(authConfig) - if err != nil { - return "", err - } - return encodedAuth, nil -} - -// resolveAuthConfigFromImage retrieves that AuthConfig using the image string -func resolveAuthConfigFromImage(ctx context.Context, cli Cli, image string) (types.AuthConfig, error) { - registryRef, err := reference.ParseNormalizedNamed(image) - if err != nil { - return types.AuthConfig{}, err - } - repoInfo, err := registry.ParseRepositoryInfo(registryRef) - if err != nil { - return types.AuthConfig{}, err - } - return ResolveAuthConfig(ctx, cli, repoInfo.Index), nil -} diff --git a/cli/command/registry/login.go b/cli/command/registry/login.go deleted file mode 100644 index 343d107dc2..0000000000 --- a/cli/command/registry/login.go +++ /dev/null @@ -1,87 +0,0 @@ -package registry - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/registry" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type loginOptions struct { - serverAddress string - user string - password string - email string -} - -// NewLoginCommand creates a new `docker login` command -func NewLoginCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts loginOptions - - cmd := &cobra.Command{ - Use: "login [OPTIONS] [SERVER]", - Short: "Log in to a Docker registry", - Long: "Log in to a Docker registry.\nIf no server is specified, the default is defined by the daemon.", - Args: cli.RequiresMaxArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) > 0 { - opts.serverAddress = args[0] - } - return runLogin(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.StringVarP(&opts.user, "username", "u", "", "Username") - flags.StringVarP(&opts.password, "password", "p", "", "Password") - - // Deprecated in 1.11: Should be removed in docker 17.06 - flags.StringVarP(&opts.email, "email", "e", "", "Email") - flags.MarkDeprecated("email", "will be removed in 17.06.") - - return cmd -} - -func runLogin(dockerCli *command.DockerCli, opts loginOptions) error { - ctx := context.Background() - clnt := dockerCli.Client() - - var ( - serverAddress string - authServer = command.ElectAuthServer(ctx, dockerCli) - ) - if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace { - serverAddress = opts.serverAddress - } else { - serverAddress = authServer - } - - isDefaultRegistry := serverAddress == authServer - - authConfig, err := command.ConfigureAuth(dockerCli, opts.user, opts.password, serverAddress, isDefaultRegistry) - if err != nil { - return err - } - response, err := clnt.RegistryLogin(ctx, authConfig) - if err != nil { - return err - } - if response.IdentityToken != "" { - authConfig.Password = "" - authConfig.IdentityToken = response.IdentityToken - } - if err := dockerCli.CredentialsStore(serverAddress).Store(authConfig); err != nil { - return errors.Errorf("Error saving credentials: %v", err) - } - - if response.Status != "" { - fmt.Fprintln(dockerCli.Out(), response.Status) - } - return nil -} diff --git a/cli/command/registry/logout.go b/cli/command/registry/logout.go deleted file mode 100644 index f1f397fa08..0000000000 --- a/cli/command/registry/logout.go +++ /dev/null @@ -1,77 +0,0 @@ -package registry - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/registry" - "github.com/spf13/cobra" -) - -// NewLogoutCommand creates a new `docker logout` command -func NewLogoutCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "logout [SERVER]", - Short: "Log out from a Docker registry", - Long: "Log out from a Docker registry.\nIf no server is specified, the default is defined by the daemon.", - Args: cli.RequiresMaxArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - var serverAddress string - if len(args) > 0 { - serverAddress = args[0] - } - return runLogout(dockerCli, serverAddress) - }, - } - - return cmd -} - -func runLogout(dockerCli *command.DockerCli, serverAddress string) error { - ctx := context.Background() - var isDefaultRegistry bool - - if serverAddress == "" { - serverAddress = command.ElectAuthServer(ctx, dockerCli) - isDefaultRegistry = true - } - - var ( - loggedIn bool - regsToLogout []string - hostnameAddress = serverAddress - regsToTry = []string{serverAddress} - ) - if !isDefaultRegistry { - hostnameAddress = registry.ConvertToHostname(serverAddress) - // the tries below are kept for backward compatibility where a user could have - // saved the registry in one of the following format. - regsToTry = append(regsToTry, hostnameAddress, "http://"+hostnameAddress, "https://"+hostnameAddress) - } - - // check if we're logged in based on the records in the config file - // which means it couldn't have user/pass cause they may be in the creds store - for _, s := range regsToTry { - if _, ok := dockerCli.ConfigFile().AuthConfigs[s]; ok { - loggedIn = true - regsToLogout = append(regsToLogout, s) - } - } - - if !loggedIn { - fmt.Fprintf(dockerCli.Out(), "Not logged in to %s\n", hostnameAddress) - return nil - } - - fmt.Fprintf(dockerCli.Out(), "Removing login credentials for %s\n", hostnameAddress) - for _, r := range regsToLogout { - if err := dockerCli.CredentialsStore(r).Erase(r); err != nil { - fmt.Fprintf(dockerCli.Err(), "WARNING: could not erase credentials: %v\n", err) - } - } - - return nil -} diff --git a/cli/command/registry/search.go b/cli/command/registry/search.go deleted file mode 100644 index f534082d32..0000000000 --- a/cli/command/registry/search.go +++ /dev/null @@ -1,126 +0,0 @@ -package registry - -import ( - "fmt" - "sort" - "strings" - "text/tabwriter" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/stringutils" - "github.com/docker/docker/registry" - "github.com/spf13/cobra" -) - -type searchOptions struct { - term string - noTrunc bool - limit int - filter opts.FilterOpt - - // Deprecated - stars uint - automated bool -} - -// NewSearchCommand creates a new `docker search` command -func NewSearchCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := searchOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "search [OPTIONS] TERM", - Short: "Search the Docker Hub for images", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.term = args[0] - return runSearch(dockerCli, opts) - }, - } - - flags := cmd.Flags() - - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - flags.IntVar(&opts.limit, "limit", registry.DefaultSearchLimit, "Max number of search results") - - flags.BoolVar(&opts.automated, "automated", false, "Only show automated builds") - flags.UintVarP(&opts.stars, "stars", "s", 0, "Only displays with at least x stars") - - flags.MarkDeprecated("automated", "use --filter=is-automated=true instead") - flags.MarkDeprecated("stars", "use --filter=stars=3 instead") - - return cmd -} - -func runSearch(dockerCli *command.DockerCli, opts searchOptions) error { - indexInfo, err := registry.ParseSearchIndexInfo(opts.term) - if err != nil { - return err - } - - ctx := context.Background() - - authConfig := command.ResolveAuthConfig(ctx, dockerCli, indexInfo) - requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, indexInfo, "search") - - encodedAuth, err := command.EncodeAuthToBase64(authConfig) - if err != nil { - return err - } - - options := types.ImageSearchOptions{ - RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, - Filters: opts.filter.Value(), - Limit: opts.limit, - } - - clnt := dockerCli.Client() - - unorderedResults, err := clnt.ImageSearch(ctx, opts.term, options) - if err != nil { - return err - } - - results := searchResultsByStars(unorderedResults) - sort.Sort(results) - - w := tabwriter.NewWriter(dockerCli.Out(), 10, 1, 3, ' ', 0) - fmt.Fprintf(w, "NAME\tDESCRIPTION\tSTARS\tOFFICIAL\tAUTOMATED\n") - for _, res := range results { - // --automated and -s, --stars are deprecated since Docker 1.12 - if (opts.automated && !res.IsAutomated) || (int(opts.stars) > res.StarCount) { - continue - } - desc := strings.Replace(res.Description, "\n", " ", -1) - desc = strings.Replace(desc, "\r", " ", -1) - if !opts.noTrunc { - desc = stringutils.Ellipsis(desc, 45) - } - fmt.Fprintf(w, "%s\t%s\t%d\t", res.Name, desc, res.StarCount) - if res.IsOfficial { - fmt.Fprint(w, "[OK]") - - } - fmt.Fprint(w, "\t") - if res.IsAutomated { - fmt.Fprint(w, "[OK]") - } - fmt.Fprint(w, "\n") - } - w.Flush() - return nil -} - -// searchResultsByStars sorts search results in descending order by number of stars. -type searchResultsByStars []registrytypes.SearchResult - -func (r searchResultsByStars) Len() int { return len(r) } -func (r searchResultsByStars) Swap(i, j int) { r[i], r[j] = r[j], r[i] } -func (r searchResultsByStars) Less(i, j int) bool { return r[j].StarCount < r[i].StarCount } diff --git a/cli/command/secret/client_test.go b/cli/command/secret/client_test.go deleted file mode 100644 index bb4b412fc2..0000000000 --- a/cli/command/secret/client_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package secret - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -type fakeClient struct { - client.Client - secretCreateFunc func(swarm.SecretSpec) (types.SecretCreateResponse, error) - secretInspectFunc func(string) (swarm.Secret, []byte, error) - secretListFunc func(types.SecretListOptions) ([]swarm.Secret, error) - secretRemoveFunc func(string) error -} - -func (c *fakeClient) SecretCreate(ctx context.Context, spec swarm.SecretSpec) (types.SecretCreateResponse, error) { - if c.secretCreateFunc != nil { - return c.secretCreateFunc(spec) - } - return types.SecretCreateResponse{}, nil -} - -func (c *fakeClient) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) { - if c.secretInspectFunc != nil { - return c.secretInspectFunc(id) - } - return swarm.Secret{}, nil, nil -} - -func (c *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) { - if c.secretListFunc != nil { - return c.secretListFunc(options) - } - return []swarm.Secret{}, nil -} - -func (c *fakeClient) SecretRemove(ctx context.Context, name string) error { - if c.secretRemoveFunc != nil { - return c.secretRemoveFunc(name) - } - return nil -} diff --git a/cli/command/secret/cmd.go b/cli/command/secret/cmd.go deleted file mode 100644 index acaef4dcac..0000000000 --- a/cli/command/secret/cmd.go +++ /dev/null @@ -1,26 +0,0 @@ -package secret - -import ( - "github.com/spf13/cobra" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" -) - -// NewSecretCommand returns a cobra command for `secret` subcommands -func NewSecretCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "secret", - Short: "Manage Docker secrets", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - Tags: map[string]string{"version": "1.25"}, - } - cmd.AddCommand( - newSecretListCommand(dockerCli), - newSecretCreateCommand(dockerCli), - newSecretInspectCommand(dockerCli), - newSecretRemoveCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/secret/create.go b/cli/command/secret/create.go deleted file mode 100644 index 59b0798178..0000000000 --- a/cli/command/secret/create.go +++ /dev/null @@ -1,80 +0,0 @@ -package secret - -import ( - "fmt" - "io" - "io/ioutil" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/system" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type createOptions struct { - name string - file string - labels opts.ListOpts -} - -func newSecretCreateCommand(dockerCli command.Cli) *cobra.Command { - createOpts := createOptions{ - labels: opts.NewListOpts(opts.ValidateEnv), - } - - cmd := &cobra.Command{ - Use: "create [OPTIONS] SECRET file|-", - Short: "Create a secret from a file or STDIN as content", - Args: cli.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - createOpts.name = args[0] - createOpts.file = args[1] - return runSecretCreate(dockerCli, createOpts) - }, - } - flags := cmd.Flags() - flags.VarP(&createOpts.labels, "label", "l", "Secret labels") - - return cmd -} - -func runSecretCreate(dockerCli command.Cli, options createOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - var in io.Reader = dockerCli.In() - if options.file != "-" { - file, err := system.OpenSequential(options.file) - if err != nil { - return err - } - in = file - defer file.Close() - } - - secretData, err := ioutil.ReadAll(in) - if err != nil { - return errors.Errorf("Error reading content from %q: %v", options.file, err) - } - - spec := swarm.SecretSpec{ - Annotations: swarm.Annotations{ - Name: options.name, - Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), - }, - Data: secretData, - } - - r, err := client.SecretCreate(ctx, spec) - if err != nil { - return err - } - - fmt.Fprintln(dockerCli.Out(), r.ID) - return nil -} diff --git a/cli/command/secret/create_test.go b/cli/command/secret/create_test.go deleted file mode 100644 index 0e9c1cd4af..0000000000 --- a/cli/command/secret/create_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package secret - -import ( - "bytes" - "io/ioutil" - "path/filepath" - "strings" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -const secretDataFile = "secret-create-with-name.golden" - -func TestSecretCreateErrors(t *testing.T) { - testCases := []struct { - args []string - secretCreateFunc func(swarm.SecretSpec) (types.SecretCreateResponse, error) - expectedError string - }{ - { - args: []string{"too_few"}, - expectedError: "requires exactly 2 argument(s)", - }, - {args: []string{"too", "many", "arguments"}, - expectedError: "requires exactly 2 argument(s)", - }, - { - args: []string{"name", filepath.Join("testdata", secretDataFile)}, - secretCreateFunc: func(secretSpec swarm.SecretSpec) (types.SecretCreateResponse, error) { - return types.SecretCreateResponse{}, errors.Errorf("error creating secret") - }, - expectedError: "error creating secret", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newSecretCreateCommand( - test.NewFakeCli(&fakeClient{ - secretCreateFunc: tc.secretCreateFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSecretCreateWithName(t *testing.T) { - name := "foo" - buf := new(bytes.Buffer) - var actual []byte - cli := test.NewFakeCli(&fakeClient{ - secretCreateFunc: func(spec swarm.SecretSpec) (types.SecretCreateResponse, error) { - if spec.Name != name { - return types.SecretCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name) - } - - actual = spec.Data - - return types.SecretCreateResponse{ - ID: "ID-" + spec.Name, - }, nil - }, - }, buf) - - cmd := newSecretCreateCommand(cli) - cmd.SetArgs([]string{name, filepath.Join("testdata", secretDataFile)}) - assert.NoError(t, cmd.Execute()) - expected := golden.Get(t, actual, secretDataFile) - assert.Equal(t, expected, actual) - assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String())) -} - -func TestSecretCreateWithLabels(t *testing.T) { - expectedLabels := map[string]string{ - "lbl1": "Label-foo", - "lbl2": "Label-bar", - } - name := "foo" - - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - secretCreateFunc: func(spec swarm.SecretSpec) (types.SecretCreateResponse, error) { - if spec.Name != name { - return types.SecretCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name) - } - - if !compareMap(spec.Labels, expectedLabels) { - return types.SecretCreateResponse{}, errors.Errorf("expected labels %v, got %v", expectedLabels, spec.Labels) - } - - return types.SecretCreateResponse{ - ID: "ID-" + spec.Name, - }, nil - }, - }, buf) - - cmd := newSecretCreateCommand(cli) - cmd.SetArgs([]string{name, filepath.Join("testdata", secretDataFile)}) - cmd.Flags().Set("label", "lbl1=Label-foo") - cmd.Flags().Set("label", "lbl2=Label-bar") - assert.NoError(t, cmd.Execute()) - assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String())) -} - -func compareMap(actual map[string]string, expected map[string]string) bool { - if len(actual) != len(expected) { - return false - } - for key, value := range actual { - if expectedValue, ok := expected[key]; ok { - if expectedValue != value { - return false - } - } else { - return false - } - } - return true -} diff --git a/cli/command/secret/inspect.go b/cli/command/secret/inspect.go deleted file mode 100644 index 8b3c3c682e..0000000000 --- a/cli/command/secret/inspect.go +++ /dev/null @@ -1,41 +0,0 @@ -package secret - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type inspectOptions struct { - names []string - format string -} - -func newSecretInspectCommand(dockerCli command.Cli) *cobra.Command { - opts := inspectOptions{} - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] SECRET [SECRET...]", - Short: "Display detailed information on one or more secrets", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.names = args - return runSecretInspect(dockerCli, opts) - }, - } - - cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - return cmd -} - -func runSecretInspect(dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - getRef := func(id string) (interface{}, []byte, error) { - return client.SecretInspectWithRaw(ctx, id) - } - - return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getRef) -} diff --git a/cli/command/secret/inspect_test.go b/cli/command/secret/inspect_test.go deleted file mode 100644 index 52b9a1cef0..0000000000 --- a/cli/command/secret/inspect_test.go +++ /dev/null @@ -1,150 +0,0 @@ -package secret - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestSecretInspectErrors(t *testing.T) { - testCases := []struct { - args []string - flags map[string]string - secretInspectFunc func(secretID string) (swarm.Secret, []byte, error) - expectedError string - }{ - { - expectedError: "requires at least 1 argument", - }, - { - args: []string{"foo"}, - secretInspectFunc: func(secretID string) (swarm.Secret, []byte, error) { - return swarm.Secret{}, nil, errors.Errorf("error while inspecting the secret") - }, - expectedError: "error while inspecting the secret", - }, - { - args: []string{"foo"}, - flags: map[string]string{ - "format": "{{invalid format}}", - }, - expectedError: "Template parsing error", - }, - { - args: []string{"foo", "bar"}, - secretInspectFunc: func(secretID string) (swarm.Secret, []byte, error) { - if secretID == "foo" { - return *Secret(SecretName("foo")), nil, nil - } - return swarm.Secret{}, nil, errors.Errorf("error while inspecting the secret") - }, - expectedError: "error while inspecting the secret", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newSecretInspectCommand( - test.NewFakeCli(&fakeClient{ - secretInspectFunc: tc.secretInspectFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSecretInspectWithoutFormat(t *testing.T) { - testCases := []struct { - name string - args []string - secretInspectFunc func(secretID string) (swarm.Secret, []byte, error) - }{ - { - name: "single-secret", - args: []string{"foo"}, - secretInspectFunc: func(name string) (swarm.Secret, []byte, error) { - if name != "foo" { - return swarm.Secret{}, nil, errors.Errorf("Invalid name, expected %s, got %s", "foo", name) - } - return *Secret(SecretID("ID-foo"), SecretName("foo")), nil, nil - }, - }, - { - name: "multiple-secrets-with-labels", - args: []string{"foo", "bar"}, - secretInspectFunc: func(name string) (swarm.Secret, []byte, error) { - return *Secret(SecretID("ID-"+name), SecretName(name), SecretLabels(map[string]string{ - "label1": "label-foo", - })), nil, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newSecretInspectCommand( - test.NewFakeCli(&fakeClient{ - secretInspectFunc: tc.secretInspectFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("secret-inspect-without-format.%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} - -func TestSecretInspectWithFormat(t *testing.T) { - secretInspectFunc := func(name string) (swarm.Secret, []byte, error) { - return *Secret(SecretName("foo"), SecretLabels(map[string]string{ - "label1": "label-foo", - })), nil, nil - } - testCases := []struct { - name string - format string - args []string - secretInspectFunc func(name string) (swarm.Secret, []byte, error) - }{ - { - name: "simple-template", - format: "{{.Spec.Name}}", - args: []string{"foo"}, - secretInspectFunc: secretInspectFunc, - }, - { - name: "json-template", - format: "{{json .Spec.Labels}}", - args: []string{"foo"}, - secretInspectFunc: secretInspectFunc, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newSecretInspectCommand( - test.NewFakeCli(&fakeClient{ - secretInspectFunc: tc.secretInspectFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - cmd.Flags().Set("format", tc.format) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("secret-inspect-with-format.%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} diff --git a/cli/command/secret/ls.go b/cli/command/secret/ls.go deleted file mode 100644 index 384ee26509..0000000000 --- a/cli/command/secret/ls.go +++ /dev/null @@ -1,61 +0,0 @@ -package secret - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type listOptions struct { - quiet bool - format string - filter opts.FilterOpt -} - -func newSecretListCommand(dockerCli command.Cli) *cobra.Command { - opts := listOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ls [OPTIONS]", - Aliases: []string{"list"}, - Short: "List secrets", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runSecretList(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") - flags.StringVarP(&opts.format, "format", "", "", "Pretty-print secrets using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - - return cmd -} - -func runSecretList(dockerCli command.Cli, opts listOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - secrets, err := client.SecretList(ctx, types.SecretListOptions{Filters: opts.filter.Value()}) - if err != nil { - return err - } - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().SecretFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().SecretFormat - } else { - format = formatter.TableFormatKey - } - } - secretCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewSecretFormat(format, opts.quiet), - } - return formatter.SecretWrite(secretCtx, secrets) -} diff --git a/cli/command/secret/ls_test.go b/cli/command/secret/ls_test.go deleted file mode 100644 index cb0510adff..0000000000 --- a/cli/command/secret/ls_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package secret - -import ( - "bytes" - "io/ioutil" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/config/configfile" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestSecretListErrors(t *testing.T) { - testCases := []struct { - args []string - secretListFunc func(types.SecretListOptions) ([]swarm.Secret, error) - expectedError string - }{ - { - args: []string{"foo"}, - expectedError: "accepts no argument", - }, - { - secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { - return []swarm.Secret{}, errors.Errorf("error listing secrets") - }, - expectedError: "error listing secrets", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newSecretListCommand( - test.NewFakeCli(&fakeClient{ - secretListFunc: tc.secretListFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSecretList(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { - return []swarm.Secret{ - *Secret(SecretID("ID-foo"), - SecretName("foo"), - SecretVersion(swarm.Version{Index: 10}), - SecretCreatedAt(time.Now().Add(-2*time.Hour)), - SecretUpdatedAt(time.Now().Add(-1*time.Hour)), - ), - *Secret(SecretID("ID-bar"), - SecretName("bar"), - SecretVersion(swarm.Version{Index: 11}), - SecretCreatedAt(time.Now().Add(-2*time.Hour)), - SecretUpdatedAt(time.Now().Add(-1*time.Hour)), - ), - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newSecretListCommand(cli) - cmd.SetOutput(buf) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "secret-list.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) -} - -func TestSecretListWithQuietOption(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { - return []swarm.Secret{ - *Secret(SecretID("ID-foo"), SecretName("foo")), - *Secret(SecretID("ID-bar"), SecretName("bar"), SecretLabels(map[string]string{ - "label": "label-bar", - })), - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newSecretListCommand(cli) - cmd.Flags().Set("quiet", "true") - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "secret-list-with-quiet-option.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) -} - -func TestSecretListWithConfigFormat(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { - return []swarm.Secret{ - *Secret(SecretID("ID-foo"), SecretName("foo")), - *Secret(SecretID("ID-bar"), SecretName("bar"), SecretLabels(map[string]string{ - "label": "label-bar", - })), - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{ - SecretFormat: "{{ .Name }} {{ .Labels }}", - }) - cmd := newSecretListCommand(cli) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "secret-list-with-config-format.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) -} - -func TestSecretListWithFormat(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { - return []swarm.Secret{ - *Secret(SecretID("ID-foo"), SecretName("foo")), - *Secret(SecretID("ID-bar"), SecretName("bar"), SecretLabels(map[string]string{ - "label": "label-bar", - })), - }, nil - }, - }, buf) - cmd := newSecretListCommand(cli) - cmd.Flags().Set("format", "{{ .Name }} {{ .Labels }}") - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "secret-list-with-format.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) -} - -func TestSecretListWithFilter(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { - assert.Equal(t, "foo", options.Filters.Get("name")[0], "foo") - assert.Equal(t, "lbl1=Label-bar", options.Filters.Get("label")[0]) - return []swarm.Secret{ - *Secret(SecretID("ID-foo"), - SecretName("foo"), - SecretVersion(swarm.Version{Index: 10}), - SecretCreatedAt(time.Now().Add(-2*time.Hour)), - SecretUpdatedAt(time.Now().Add(-1*time.Hour)), - ), - *Secret(SecretID("ID-bar"), - SecretName("bar"), - SecretVersion(swarm.Version{Index: 11}), - SecretCreatedAt(time.Now().Add(-2*time.Hour)), - SecretUpdatedAt(time.Now().Add(-1*time.Hour)), - ), - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newSecretListCommand(cli) - cmd.Flags().Set("filter", "name=foo") - cmd.Flags().Set("filter", "label=lbl1=Label-bar") - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "secret-list-with-filter.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) -} diff --git a/cli/command/secret/remove.go b/cli/command/secret/remove.go deleted file mode 100644 index a4b501d176..0000000000 --- a/cli/command/secret/remove.go +++ /dev/null @@ -1,53 +0,0 @@ -package secret - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type removeOptions struct { - names []string -} - -func newSecretRemoveCommand(dockerCli command.Cli) *cobra.Command { - return &cobra.Command{ - Use: "rm SECRET [SECRET...]", - Aliases: []string{"remove"}, - Short: "Remove one or more secrets", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts := removeOptions{ - names: args, - } - return runSecretRemove(dockerCli, opts) - }, - } -} - -func runSecretRemove(dockerCli command.Cli, opts removeOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - var errs []string - - for _, name := range opts.names { - if err := client.SecretRemove(ctx, name); err != nil { - errs = append(errs, err.Error()) - continue - } - - fmt.Fprintln(dockerCli.Out(), name) - } - - if len(errs) > 0 { - return errors.Errorf("%s", strings.Join(errs, "\n")) - } - - return nil -} diff --git a/cli/command/secret/remove_test.go b/cli/command/secret/remove_test.go deleted file mode 100644 index b8bbb5e6f5..0000000000 --- a/cli/command/secret/remove_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package secret - -import ( - "bytes" - "io/ioutil" - "strings" - "testing" - - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestSecretRemoveErrors(t *testing.T) { - testCases := []struct { - args []string - secretRemoveFunc func(string) error - expectedError string - }{ - { - args: []string{}, - expectedError: "requires at least 1 argument(s).", - }, - { - args: []string{"foo"}, - secretRemoveFunc: func(name string) error { - return errors.Errorf("error removing secret") - }, - expectedError: "error removing secret", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newSecretRemoveCommand( - test.NewFakeCli(&fakeClient{ - secretRemoveFunc: tc.secretRemoveFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSecretRemoveWithName(t *testing.T) { - names := []string{"foo", "bar"} - buf := new(bytes.Buffer) - var removedSecrets []string - cli := test.NewFakeCli(&fakeClient{ - secretRemoveFunc: func(name string) error { - removedSecrets = append(removedSecrets, name) - return nil - }, - }, buf) - cmd := newSecretRemoveCommand(cli) - cmd.SetArgs(names) - assert.NoError(t, cmd.Execute()) - assert.Equal(t, names, strings.Split(strings.TrimSpace(buf.String()), "\n")) - assert.Equal(t, names, removedSecrets) -} - -func TestSecretRemoveContinueAfterError(t *testing.T) { - names := []string{"foo", "bar"} - buf := new(bytes.Buffer) - var removedSecrets []string - - cli := test.NewFakeCli(&fakeClient{ - secretRemoveFunc: func(name string) error { - removedSecrets = append(removedSecrets, name) - if name == "foo" { - return errors.Errorf("error removing secret: %s", name) - } - return nil - }, - }, buf) - - cmd := newSecretRemoveCommand(cli) - cmd.SetArgs(names) - assert.EqualError(t, cmd.Execute(), "error removing secret: foo") - assert.Equal(t, names, removedSecrets) -} diff --git a/cli/command/secret/testdata/secret-create-with-name.golden b/cli/command/secret/testdata/secret-create-with-name.golden deleted file mode 100644 index 788642a93a..0000000000 --- a/cli/command/secret/testdata/secret-create-with-name.golden +++ /dev/null @@ -1 +0,0 @@ -secret_foo_bar diff --git a/cli/command/secret/testdata/secret-inspect-with-format.json-template.golden b/cli/command/secret/testdata/secret-inspect-with-format.json-template.golden deleted file mode 100644 index aab678f85d..0000000000 --- a/cli/command/secret/testdata/secret-inspect-with-format.json-template.golden +++ /dev/null @@ -1 +0,0 @@ -{"label1":"label-foo"} diff --git a/cli/command/secret/testdata/secret-inspect-with-format.simple-template.golden b/cli/command/secret/testdata/secret-inspect-with-format.simple-template.golden deleted file mode 100644 index 257cc5642c..0000000000 --- a/cli/command/secret/testdata/secret-inspect-with-format.simple-template.golden +++ /dev/null @@ -1 +0,0 @@ -foo diff --git a/cli/command/secret/testdata/secret-inspect-without-format.multiple-secrets-with-labels.golden b/cli/command/secret/testdata/secret-inspect-without-format.multiple-secrets-with-labels.golden deleted file mode 100644 index 6887c185f1..0000000000 --- a/cli/command/secret/testdata/secret-inspect-without-format.multiple-secrets-with-labels.golden +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "ID": "ID-foo", - "Version": {}, - "CreatedAt": "0001-01-01T00:00:00Z", - "UpdatedAt": "0001-01-01T00:00:00Z", - "Spec": { - "Name": "foo", - "Labels": { - "label1": "label-foo" - } - } - }, - { - "ID": "ID-bar", - "Version": {}, - "CreatedAt": "0001-01-01T00:00:00Z", - "UpdatedAt": "0001-01-01T00:00:00Z", - "Spec": { - "Name": "bar", - "Labels": { - "label1": "label-foo" - } - } - } -] diff --git a/cli/command/secret/testdata/secret-inspect-without-format.single-secret.golden b/cli/command/secret/testdata/secret-inspect-without-format.single-secret.golden deleted file mode 100644 index ea42ec6f4f..0000000000 --- a/cli/command/secret/testdata/secret-inspect-without-format.single-secret.golden +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "ID": "ID-foo", - "Version": {}, - "CreatedAt": "0001-01-01T00:00:00Z", - "UpdatedAt": "0001-01-01T00:00:00Z", - "Spec": { - "Name": "foo", - "Labels": null - } - } -] diff --git a/cli/command/secret/testdata/secret-list-with-config-format.golden b/cli/command/secret/testdata/secret-list-with-config-format.golden deleted file mode 100644 index 9a47538804..0000000000 --- a/cli/command/secret/testdata/secret-list-with-config-format.golden +++ /dev/null @@ -1,2 +0,0 @@ -foo -bar label=label-bar diff --git a/cli/command/secret/testdata/secret-list-with-filter.golden b/cli/command/secret/testdata/secret-list-with-filter.golden deleted file mode 100644 index 29983de8e9..0000000000 --- a/cli/command/secret/testdata/secret-list-with-filter.golden +++ /dev/null @@ -1,3 +0,0 @@ -ID NAME CREATED UPDATED -ID-foo foo 2 hours ago About an hour ago -ID-bar bar 2 hours ago About an hour ago diff --git a/cli/command/secret/testdata/secret-list-with-format.golden b/cli/command/secret/testdata/secret-list-with-format.golden deleted file mode 100644 index 9a47538804..0000000000 --- a/cli/command/secret/testdata/secret-list-with-format.golden +++ /dev/null @@ -1,2 +0,0 @@ -foo -bar label=label-bar diff --git a/cli/command/secret/testdata/secret-list-with-quiet-option.golden b/cli/command/secret/testdata/secret-list-with-quiet-option.golden deleted file mode 100644 index 83fb6e8979..0000000000 --- a/cli/command/secret/testdata/secret-list-with-quiet-option.golden +++ /dev/null @@ -1,2 +0,0 @@ -ID-foo -ID-bar diff --git a/cli/command/secret/testdata/secret-list.golden b/cli/command/secret/testdata/secret-list.golden deleted file mode 100644 index 29983de8e9..0000000000 --- a/cli/command/secret/testdata/secret-list.golden +++ /dev/null @@ -1,3 +0,0 @@ -ID NAME CREATED UPDATED -ID-foo foo 2 hours ago About an hour ago -ID-bar bar 2 hours ago About an hour ago diff --git a/cli/command/service/cmd.go b/cli/command/service/cmd.go deleted file mode 100644 index 51208b80c2..0000000000 --- a/cli/command/service/cmd.go +++ /dev/null @@ -1,30 +0,0 @@ -package service - -import ( - "github.com/spf13/cobra" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" -) - -// NewServiceCommand returns a cobra command for `service` subcommands -func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "service", - Short: "Manage services", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - Tags: map[string]string{"version": "1.24"}, - } - cmd.AddCommand( - newCreateCommand(dockerCli), - newInspectCommand(dockerCli), - newPsCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - newScaleCommand(dockerCli), - newUpdateCommand(dockerCli), - newLogsCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/service/create.go b/cli/command/service/create.go deleted file mode 100644 index bb2a1fe3b7..0000000000 --- a/cli/command/service/create.go +++ /dev/null @@ -1,118 +0,0 @@ -package service - -import ( - "fmt" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "golang.org/x/net/context" -) - -func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := newServiceOptions() - - cmd := &cobra.Command{ - Use: "create [OPTIONS] IMAGE [COMMAND] [ARG...]", - Short: "Create a new service", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.image = args[0] - if len(args) > 1 { - opts.args = args[1:] - } - return runCreate(dockerCli, cmd.Flags(), opts) - }, - } - flags := cmd.Flags() - flags.StringVar(&opts.mode, flagMode, "replicated", "Service mode (replicated or global)") - flags.StringVar(&opts.name, flagName, "", "Service name") - - addServiceFlags(flags, opts, buildServiceDefaultFlagMapping()) - - flags.VarP(&opts.labels, flagLabel, "l", "Service labels") - flags.Var(&opts.containerLabels, flagContainerLabel, "Container labels") - flags.VarP(&opts.env, flagEnv, "e", "Set environment variables") - flags.Var(&opts.envFile, flagEnvFile, "Read in a file of environment variables") - flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service") - flags.Var(&opts.constraints, flagConstraint, "Placement constraints") - flags.Var(&opts.placementPrefs, flagPlacementPref, "Add a placement preference") - flags.SetAnnotation(flagPlacementPref, "version", []string{"1.28"}) - flags.Var(&opts.networks, flagNetwork, "Network attachments") - flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service") - flags.SetAnnotation(flagSecret, "version", []string{"1.25"}) - flags.VarP(&opts.endpoint.publishPorts, flagPublish, "p", "Publish a port as a node port") - flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container") - flags.SetAnnotation(flagGroup, "version", []string{"1.25"}) - flags.Var(&opts.dns, flagDNS, "Set custom DNS servers") - flags.SetAnnotation(flagDNS, "version", []string{"1.25"}) - flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options") - flags.SetAnnotation(flagDNSOption, "version", []string{"1.25"}) - flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains") - flags.SetAnnotation(flagDNSSearch, "version", []string{"1.25"}) - flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)") - flags.SetAnnotation(flagHost, "version", []string{"1.25"}) - - flags.SetInterspersed(false) - return cmd -} - -func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *serviceOptions) error { - apiClient := dockerCli.Client() - createOpts := types.ServiceCreateOptions{} - - ctx := context.Background() - - service, err := opts.ToService(ctx, apiClient, flags) - if err != nil { - return err - } - - specifiedSecrets := opts.secrets.Value() - if len(specifiedSecrets) > 0 { - // parse and validate secrets - secrets, err := ParseSecrets(apiClient, specifiedSecrets) - if err != nil { - return err - } - service.TaskTemplate.ContainerSpec.Secrets = secrets - - } - - if err := resolveServiceImageDigest(dockerCli, &service); err != nil { - return err - } - - // only send auth if flag was set - if opts.registryAuth { - // Retrieve encoded auth token from the image reference - encodedAuth, err := command.RetrieveAuthTokenFromImage(ctx, dockerCli, opts.image) - if err != nil { - return err - } - createOpts.EncodedRegistryAuth = encodedAuth - } - - response, err := apiClient.ServiceCreate(ctx, service, createOpts) - if err != nil { - return err - } - - for _, warning := range response.Warnings { - fmt.Fprintln(dockerCli.Err(), warning) - } - - fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID) - - if opts.detach { - if !flags.Changed("detach") { - fmt.Fprintln(dockerCli.Err(), "Since --detach=false was not specified, tasks will be created in the background.\n"+ - "In a future release, --detach=false will become the default.") - } - return nil - } - - return waitOnService(ctx, dockerCli, response.ID, opts) -} diff --git a/cli/command/service/helpers.go b/cli/command/service/helpers.go deleted file mode 100644 index 2289369908..0000000000 --- a/cli/command/service/helpers.go +++ /dev/null @@ -1,39 +0,0 @@ -package service - -import ( - "io" - - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/service/progress" - "github.com/docker/docker/pkg/jsonmessage" - "golang.org/x/net/context" -) - -// waitOnService waits for the service to converge. It outputs a progress bar, -// if appopriate based on the CLI flags. -func waitOnService(ctx context.Context, dockerCli *command.DockerCli, serviceID string, opts *serviceOptions) error { - errChan := make(chan error, 1) - pipeReader, pipeWriter := io.Pipe() - - go func() { - errChan <- progress.ServiceProgress(ctx, dockerCli.Client(), serviceID, pipeWriter) - }() - - if opts.quiet { - go func() { - for { - var buf [1024]byte - if _, err := pipeReader.Read(buf[:]); err != nil { - return - } - } - }() - return <-errChan - } - - err := jsonmessage.DisplayJSONMessagesToStream(pipeReader, dockerCli.Out(), nil) - if err == nil { - err = <-errChan - } - return err -} diff --git a/cli/command/service/inspect.go b/cli/command/service/inspect.go deleted file mode 100644 index fae24eeaf1..0000000000 --- a/cli/command/service/inspect.go +++ /dev/null @@ -1,94 +0,0 @@ -package service - -import ( - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - apiclient "github.com/docker/docker/client" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type inspectOptions struct { - refs []string - format string - pretty bool -} - -func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts inspectOptions - - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] SERVICE [SERVICE...]", - Short: "Display detailed information on one or more services", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.refs = args - - if opts.pretty && len(opts.format) > 0 { - return errors.Errorf("--format is incompatible with human friendly format") - } - return runInspect(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format") - return cmd -} - -func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - if opts.pretty { - opts.format = "pretty" - } - - getRef := func(ref string) (interface{}, []byte, error) { - // Service inspect shows defaults values in empty fields. - service, _, err := client.ServiceInspectWithRaw(ctx, ref, types.ServiceInspectOptions{InsertDefaults: true}) - if err == nil || !apiclient.IsErrServiceNotFound(err) { - return service, nil, err - } - return nil, nil, errors.Errorf("Error: no such service: %s", ref) - } - - getNetwork := func(ref string) (interface{}, []byte, error) { - network, _, err := client.NetworkInspectWithRaw(ctx, ref, false) - if err == nil || !apiclient.IsErrNetworkNotFound(err) { - return network, nil, err - } - return nil, nil, errors.Errorf("Error: no such network: %s", ref) - } - - f := opts.format - if len(f) == 0 { - f = "raw" - if len(dockerCli.ConfigFile().ServiceInspectFormat) > 0 { - f = dockerCli.ConfigFile().ServiceInspectFormat - } - } - - // check if the user is trying to apply a template to the pretty format, which - // is not supported - if strings.HasPrefix(f, "pretty") && f != "pretty" { - return errors.Errorf("Cannot supply extra formatting options to the pretty template") - } - - serviceCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewServiceFormat(f), - } - - if err := formatter.ServiceInspectWrite(serviceCtx, opts.refs, getRef, getNetwork); err != nil { - return cli.StatusError{StatusCode: 1, Status: err.Error()} - } - return nil -} diff --git a/cli/command/service/inspect_test.go b/cli/command/service/inspect_test.go deleted file mode 100644 index c5bda7dcd9..0000000000 --- a/cli/command/service/inspect_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package service - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command/formatter" - "github.com/stretchr/testify/assert" -) - -func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) string { - b := new(bytes.Buffer) - - endpointSpec := &swarm.EndpointSpec{ - Mode: "vip", - Ports: []swarm.PortConfig{ - { - Protocol: swarm.PortConfigProtocolTCP, - TargetPort: 5000, - }, - }, - } - - two := uint64(2) - - s := swarm.Service{ - ID: "de179gar9d0o7ltdybungplod", - Meta: swarm.Meta{ - Version: swarm.Version{Index: 315}, - CreatedAt: now, - UpdatedAt: now, - }, - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: "my_service", - Labels: map[string]string{"com.label": "foo"}, - }, - TaskTemplate: swarm.TaskSpec{ - ContainerSpec: swarm.ContainerSpec{ - Image: "foo/bar@sha256:this_is_a_test", - }, - Networks: []swarm.NetworkAttachmentConfig{ - { - Target: "5vpyomhb6ievnk0i0o60gcnei", - Aliases: []string{"web"}, - }, - }, - }, - Mode: swarm.ServiceMode{ - Replicated: &swarm.ReplicatedService{ - Replicas: &two, - }, - }, - EndpointSpec: endpointSpec, - }, - Endpoint: swarm.Endpoint{ - Spec: *endpointSpec, - Ports: []swarm.PortConfig{ - { - Protocol: swarm.PortConfigProtocolTCP, - TargetPort: 5000, - PublishedPort: 30000, - }, - }, - VirtualIPs: []swarm.EndpointVirtualIP{ - { - NetworkID: "6o4107cj2jx9tihgb0jyts6pj", - Addr: "10.255.0.4/16", - }, - }, - }, - UpdateStatus: &swarm.UpdateStatus{ - StartedAt: &now, - CompletedAt: &now, - }, - } - - ctx := formatter.Context{ - Output: b, - Format: format, - } - - err := formatter.ServiceInspectWrite(ctx, []string{"de179gar9d0o7ltdybungplod"}, - func(ref string) (interface{}, []byte, error) { - return s, nil, nil - }, - func(ref string) (interface{}, []byte, error) { - return types.NetworkResource{ - ID: "5vpyomhb6ievnk0i0o60gcnei", - Name: "mynetwork", - }, nil, nil - }, - ) - if err != nil { - t.Fatal(err) - } - return b.String() -} - -func TestPrettyPrintWithNoUpdateConfig(t *testing.T) { - s := formatServiceInspect(t, formatter.NewServiceFormat("pretty"), time.Now()) - if strings.Contains(s, "UpdateStatus") { - t.Fatal("Pretty print failed before parsing UpdateStatus") - } - if !strings.Contains(s, "mynetwork") { - t.Fatal("network name not found in inspect output") - } -} - -func TestJSONFormatWithNoUpdateConfig(t *testing.T) { - now := time.Now() - // s1: [{"ID":..}] - // s2: {"ID":..} - s1 := formatServiceInspect(t, formatter.NewServiceFormat(""), now) - t.Log("// s1") - t.Logf("%s", s1) - s2 := formatServiceInspect(t, formatter.NewServiceFormat("{{json .}}"), now) - t.Log("// s2") - t.Logf("%s", s2) - var m1Wrap []map[string]interface{} - if err := json.Unmarshal([]byte(s1), &m1Wrap); err != nil { - t.Fatal(err) - } - if len(m1Wrap) != 1 { - t.Fatalf("strange s1=%s", s1) - } - m1 := m1Wrap[0] - t.Logf("m1=%+v", m1) - var m2 map[string]interface{} - if err := json.Unmarshal([]byte(s2), &m2); err != nil { - t.Fatal(err) - } - t.Logf("m2=%+v", m2) - assert.Equal(t, m1, m2) -} diff --git a/cli/command/service/list.go b/cli/command/service/list.go deleted file mode 100644 index 9c4987806f..0000000000 --- a/cli/command/service/list.go +++ /dev/null @@ -1,129 +0,0 @@ -package service - -import ( - "fmt" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type listOptions struct { - quiet bool - format string - filter opts.FilterOpt -} - -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := listOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ls [OPTIONS]", - Aliases: []string{"list"}, - Short: "List services", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") - flags.StringVar(&opts.format, "format", "", "Pretty-print services using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - - return cmd -} - -func runList(dockerCli *command.DockerCli, opts listOptions) error { - ctx := context.Background() - client := dockerCli.Client() - - serviceFilters := opts.filter.Value() - services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceFilters}) - if err != nil { - return err - } - - info := map[string]formatter.ServiceListInfo{} - if len(services) > 0 && !opts.quiet { - // only non-empty services and not quiet, should we call TaskList and NodeList api - taskFilter := filters.NewArgs() - for _, service := range services { - taskFilter.Add("service", service.ID) - } - - tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter}) - if err != nil { - return err - } - - nodes, err := client.NodeList(ctx, types.NodeListOptions{}) - if err != nil { - return err - } - - info = GetServicesStatus(services, nodes, tasks) - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().ServicesFormat - } else { - format = formatter.TableFormatKey - } - } - - servicesCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewServiceListFormat(format, opts.quiet), - } - return formatter.ServiceListWrite(servicesCtx, services, info) -} - -// GetServicesStatus returns a map of mode and replicas -func GetServicesStatus(services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) map[string]formatter.ServiceListInfo { - running := map[string]int{} - tasksNoShutdown := map[string]int{} - - activeNodes := make(map[string]struct{}) - for _, n := range nodes { - if n.Status.State != swarm.NodeStateDown { - activeNodes[n.ID] = struct{}{} - } - } - - for _, task := range tasks { - if task.DesiredState != swarm.TaskStateShutdown { - tasksNoShutdown[task.ServiceID]++ - } - - if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == swarm.TaskStateRunning { - running[task.ServiceID]++ - } - } - - info := map[string]formatter.ServiceListInfo{} - for _, service := range services { - info[service.ID] = formatter.ServiceListInfo{} - if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { - info[service.ID] = formatter.ServiceListInfo{ - Mode: "replicated", - Replicas: fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas), - } - } else if service.Spec.Mode.Global != nil { - info[service.ID] = formatter.ServiceListInfo{ - Mode: "global", - Replicas: fmt.Sprintf("%d/%d", running[service.ID], tasksNoShutdown[service.ID]), - } - } - } - return info -} diff --git a/cli/command/service/logs.go b/cli/command/service/logs.go deleted file mode 100644 index 32a3f3557a..0000000000 --- a/cli/command/service/logs.go +++ /dev/null @@ -1,301 +0,0 @@ -package service - -import ( - "bytes" - "fmt" - "io" - "strconv" - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/idresolver" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/stdcopy" - "github.com/docker/docker/pkg/stringid" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type logsOptions struct { - noResolve bool - noTrunc bool - noTaskIDs bool - follow bool - since string - timestamps bool - tail string - - target string -} - -// TODO(dperny) the whole CLI for this is kind of a mess IMHOIRL and it needs -// to be refactored agressively. There may be changes to the implementation of -// details, which will be need to be reflected in this code. The refactoring -// should be put off until we make those changes, tho, because I think the -// decisions made WRT details will impact the design of the CLI. -func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts logsOptions - - cmd := &cobra.Command{ - Use: "logs [OPTIONS] SERVICE|TASK", - Short: "Fetch the logs of a service or task", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.target = args[0] - return runLogs(dockerCli, &opts) - }, - Tags: map[string]string{"version": "1.29"}, - } - - flags := cmd.Flags() - // options specific to service logs - flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names in output") - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") - flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs in output") - // options identical to container logs - flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") - flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)") - flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") - flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs") - return cmd -} - -func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { - ctx := context.Background() - - options := types.ContainerLogsOptions{ - ShowStdout: true, - ShowStderr: true, - Since: opts.since, - Timestamps: opts.timestamps, - Follow: opts.follow, - Tail: opts.tail, - Details: true, - } - - cli := dockerCli.Client() - - var ( - maxLength = 1 - responseBody io.ReadCloser - tty bool - ) - - service, _, err := cli.ServiceInspectWithRaw(ctx, opts.target, types.ServiceInspectOptions{}) - if err != nil { - // if it's any error other than service not found, it's Real - if !client.IsErrServiceNotFound(err) { - return err - } - task, _, err := cli.TaskInspectWithRaw(ctx, opts.target) - if err != nil { - if client.IsErrTaskNotFound(err) { - // if the task isn't found, rewrite the error to be clear - // that we looked for services AND tasks and found none - err = fmt.Errorf("no such task or service") - } - return err - } - tty = task.Spec.ContainerSpec.TTY - // TODO(dperny) hot fix until we get a nice details system squared away, - // ignores details (including task context) if we have a TTY log - // if we don't do this, we'll vomit the huge context verbatim into the - // TTY log lines and that's Undesirable. - if tty { - options.Details = false - } - - responseBody, err = cli.TaskLogs(ctx, opts.target, options) - if err != nil { - return err - } - - maxLength = getMaxLength(task.Slot) - } else { - tty = service.Spec.TaskTemplate.ContainerSpec.TTY - // TODO(dperny) hot fix until we get a nice details system squared away, - // ignores details (including task context) if we have a TTY log - if tty { - options.Details = false - } - - responseBody, err = cli.ServiceLogs(ctx, opts.target, options) - if err != nil { - return err - } - if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { - // if replicas are initialized, figure out if we need to pad them - replicas := *service.Spec.Mode.Replicated.Replicas - maxLength = getMaxLength(int(replicas)) - } - } - defer responseBody.Close() - - if tty { - _, err = io.Copy(dockerCli.Out(), responseBody) - return err - } - - taskFormatter := newTaskFormatter(cli, opts, maxLength) - - stdout := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Out()} - stderr := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Err()} - - // TODO(aluzzardi): Do an io.Copy for services with TTY enabled. - _, err = stdcopy.StdCopy(stdout, stderr, responseBody) - return err -} - -// getMaxLength gets the maximum length of the number in base 10 -func getMaxLength(i int) int { - return len(strconv.FormatInt(int64(i), 10)) -} - -type taskFormatter struct { - client client.APIClient - opts *logsOptions - padding int - - r *idresolver.IDResolver - cache map[logContext]string -} - -func newTaskFormatter(client client.APIClient, opts *logsOptions, padding int) *taskFormatter { - return &taskFormatter{ - client: client, - opts: opts, - padding: padding, - r: idresolver.New(client, opts.noResolve), - cache: make(map[logContext]string), - } -} - -func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string, error) { - if cached, ok := f.cache[logCtx]; ok { - return cached, nil - } - - nodeName, err := f.r.Resolve(ctx, swarm.Node{}, logCtx.nodeID) - if err != nil { - return "", err - } - - serviceName, err := f.r.Resolve(ctx, swarm.Service{}, logCtx.serviceID) - if err != nil { - return "", err - } - - task, _, err := f.client.TaskInspectWithRaw(ctx, logCtx.taskID) - if err != nil { - return "", err - } - - taskName := fmt.Sprintf("%s.%d", serviceName, task.Slot) - if !f.opts.noTaskIDs { - if f.opts.noTrunc { - taskName += fmt.Sprintf(".%s", task.ID) - } else { - taskName += fmt.Sprintf(".%s", stringid.TruncateID(task.ID)) - } - } - - padding := strings.Repeat(" ", f.padding-getMaxLength(task.Slot)) - formatted := fmt.Sprintf("%s@%s%s", taskName, nodeName, padding) - f.cache[logCtx] = formatted - return formatted, nil -} - -type logWriter struct { - ctx context.Context - opts *logsOptions - f *taskFormatter - w io.Writer -} - -func (lw *logWriter) Write(buf []byte) (int, error) { - contextIndex := 0 - numParts := 2 - if lw.opts.timestamps { - contextIndex++ - numParts++ - } - - parts := bytes.SplitN(buf, []byte(" "), numParts) - if len(parts) != numParts { - return 0, errors.Errorf("invalid context in log message: %v", string(buf)) - } - - logCtx, err := lw.parseContext(string(parts[contextIndex])) - if err != nil { - return 0, err - } - - output := []byte{} - for i, part := range parts { - // First part doesn't get space separation. - if i > 0 { - output = append(output, []byte(" ")...) - } - - if i == contextIndex { - formatted, err := lw.f.format(lw.ctx, logCtx) - if err != nil { - return 0, err - } - output = append(output, []byte(fmt.Sprintf("%s |", formatted))...) - } else { - output = append(output, part...) - } - } - _, err = lw.w.Write(output) - if err != nil { - return 0, err - } - - return len(buf), nil -} - -func (lw *logWriter) parseContext(input string) (logContext, error) { - context := make(map[string]string) - - components := strings.Split(input, ",") - for _, component := range components { - parts := strings.SplitN(component, "=", 2) - if len(parts) != 2 { - return logContext{}, errors.Errorf("invalid context: %s", input) - } - context[parts[0]] = parts[1] - } - - nodeID, ok := context["com.docker.swarm.node.id"] - if !ok { - return logContext{}, errors.Errorf("missing node id in context: %s", input) - } - - serviceID, ok := context["com.docker.swarm.service.id"] - if !ok { - return logContext{}, errors.Errorf("missing service id in context: %s", input) - } - - taskID, ok := context["com.docker.swarm.task.id"] - if !ok { - return logContext{}, errors.Errorf("missing task id in context: %s", input) - } - - return logContext{ - nodeID: nodeID, - serviceID: serviceID, - taskID: taskID, - }, nil -} - -type logContext struct { - nodeID string - serviceID string - taskID string -} diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go deleted file mode 100644 index ecc37d7b00..0000000000 --- a/cli/command/service/opts.go +++ /dev/null @@ -1,912 +0,0 @@ -package service - -import ( - "fmt" - "sort" - "strconv" - "strings" - "time" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/docker/docker/opts" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/docker/swarmkit/api" - "github.com/docker/swarmkit/api/defaults" - shlex "github.com/flynn-archive/go-shlex" - gogotypes "github.com/gogo/protobuf/types" - "github.com/pkg/errors" - "github.com/spf13/pflag" - "golang.org/x/net/context" -) - -type int64Value interface { - Value() int64 -} - -// PositiveDurationOpt is an option type for time.Duration that uses a pointer. -// It bahave similarly to DurationOpt but only allows positive duration values. -type PositiveDurationOpt struct { - DurationOpt -} - -// Set a new value on the option. Setting a negative duration value will cause -// an error to be returned. -func (d *PositiveDurationOpt) Set(s string) error { - err := d.DurationOpt.Set(s) - if err != nil { - return err - } - if *d.DurationOpt.value < 0 { - return errors.Errorf("duration cannot be negative") - } - return nil -} - -// DurationOpt is an option type for time.Duration that uses a pointer. This -// allows us to get nil values outside, instead of defaulting to 0 -type DurationOpt struct { - value *time.Duration -} - -// Set a new value on the option -func (d *DurationOpt) Set(s string) error { - v, err := time.ParseDuration(s) - d.value = &v - return err -} - -// Type returns the type of this option, which will be displayed in `--help` output -func (d *DurationOpt) Type() string { - return "duration" -} - -// String returns a string repr of this option -func (d *DurationOpt) String() string { - if d.value != nil { - return d.value.String() - } - return "" -} - -// Value returns the time.Duration -func (d *DurationOpt) Value() *time.Duration { - return d.value -} - -// Uint64Opt represents a uint64. -type Uint64Opt struct { - value *uint64 -} - -// Set a new value on the option -func (i *Uint64Opt) Set(s string) error { - v, err := strconv.ParseUint(s, 0, 64) - i.value = &v - return err -} - -// Type returns the type of this option, which will be displayed in `--help` output -func (i *Uint64Opt) Type() string { - return "uint" -} - -// String returns a string repr of this option -func (i *Uint64Opt) String() string { - if i.value != nil { - return fmt.Sprintf("%v", *i.value) - } - return "" -} - -// Value returns the uint64 -func (i *Uint64Opt) Value() *uint64 { - return i.value -} - -type floatValue float32 - -func (f *floatValue) Set(s string) error { - v, err := strconv.ParseFloat(s, 32) - *f = floatValue(v) - return err -} - -func (f *floatValue) Type() string { - return "float" -} - -func (f *floatValue) String() string { - return strconv.FormatFloat(float64(*f), 'g', -1, 32) -} - -func (f *floatValue) Value() float32 { - return float32(*f) -} - -// placementPrefOpts holds a list of placement preferences. -type placementPrefOpts struct { - prefs []swarm.PlacementPreference - strings []string -} - -func (opts *placementPrefOpts) String() string { - if len(opts.strings) == 0 { - return "" - } - return fmt.Sprintf("%v", opts.strings) -} - -// Set validates the input value and adds it to the internal slices. -// Note: in the future strategies other than "spread", may be supported, -// as well as additional comma-separated options. -func (opts *placementPrefOpts) Set(value string) error { - fields := strings.Split(value, "=") - if len(fields) != 2 { - return errors.New(`placement preference must be of the format "="`) - } - if fields[0] != "spread" { - return errors.Errorf("unsupported placement preference %s (only spread is supported)", fields[0]) - } - - opts.prefs = append(opts.prefs, swarm.PlacementPreference{ - Spread: &swarm.SpreadOver{ - SpreadDescriptor: fields[1], - }, - }) - opts.strings = append(opts.strings, value) - return nil -} - -// Type returns a string name for this Option type -func (opts *placementPrefOpts) Type() string { - return "pref" -} - -// ShlexOpt is a flag Value which parses a string as a list of shell words -type ShlexOpt []string - -// Set the value -func (s *ShlexOpt) Set(value string) error { - valueSlice, err := shlex.Split(value) - *s = ShlexOpt(valueSlice) - return err -} - -// Type returns the tyep of the value -func (s *ShlexOpt) Type() string { - return "command" -} - -func (s *ShlexOpt) String() string { - if len(*s) == 0 { - return "" - } - return fmt.Sprint(*s) -} - -// Value returns the value as a string slice -func (s *ShlexOpt) Value() []string { - return []string(*s) -} - -type updateOptions struct { - parallelism uint64 - delay time.Duration - monitor time.Duration - onFailure string - maxFailureRatio floatValue - order string -} - -func updateConfigFromDefaults(defaultUpdateConfig *api.UpdateConfig) *swarm.UpdateConfig { - defaultFailureAction := strings.ToLower(api.UpdateConfig_FailureAction_name[int32(defaultUpdateConfig.FailureAction)]) - defaultMonitor, _ := gogotypes.DurationFromProto(defaultUpdateConfig.Monitor) - return &swarm.UpdateConfig{ - Parallelism: defaultUpdateConfig.Parallelism, - Delay: defaultUpdateConfig.Delay, - Monitor: defaultMonitor, - FailureAction: defaultFailureAction, - MaxFailureRatio: defaultUpdateConfig.MaxFailureRatio, - Order: defaultOrder(defaultUpdateConfig.Order), - } -} - -func (opts updateOptions) updateConfig(flags *pflag.FlagSet) *swarm.UpdateConfig { - if !anyChanged(flags, flagUpdateParallelism, flagUpdateDelay, flagUpdateMonitor, flagUpdateFailureAction, flagUpdateMaxFailureRatio) { - return nil - } - - updateConfig := updateConfigFromDefaults(defaults.Service.Update) - - if flags.Changed(flagUpdateParallelism) { - updateConfig.Parallelism = opts.parallelism - } - if flags.Changed(flagUpdateDelay) { - updateConfig.Delay = opts.delay - } - if flags.Changed(flagUpdateMonitor) { - updateConfig.Monitor = opts.monitor - } - if flags.Changed(flagUpdateFailureAction) { - updateConfig.FailureAction = opts.onFailure - } - if flags.Changed(flagUpdateMaxFailureRatio) { - updateConfig.MaxFailureRatio = opts.maxFailureRatio.Value() - } - if flags.Changed(flagUpdateOrder) { - updateConfig.Order = opts.order - } - - return updateConfig -} - -func (opts updateOptions) rollbackConfig(flags *pflag.FlagSet) *swarm.UpdateConfig { - if !anyChanged(flags, flagRollbackParallelism, flagRollbackDelay, flagRollbackMonitor, flagRollbackFailureAction, flagRollbackMaxFailureRatio) { - return nil - } - - updateConfig := updateConfigFromDefaults(defaults.Service.Rollback) - - if flags.Changed(flagRollbackParallelism) { - updateConfig.Parallelism = opts.parallelism - } - if flags.Changed(flagRollbackDelay) { - updateConfig.Delay = opts.delay - } - if flags.Changed(flagRollbackMonitor) { - updateConfig.Monitor = opts.monitor - } - if flags.Changed(flagRollbackFailureAction) { - updateConfig.FailureAction = opts.onFailure - } - if flags.Changed(flagRollbackMaxFailureRatio) { - updateConfig.MaxFailureRatio = opts.maxFailureRatio.Value() - } - if flags.Changed(flagRollbackOrder) { - updateConfig.Order = opts.order - } - - return updateConfig -} - -type resourceOptions struct { - limitCPU opts.NanoCPUs - limitMemBytes opts.MemBytes - resCPU opts.NanoCPUs - resMemBytes opts.MemBytes -} - -func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements { - return &swarm.ResourceRequirements{ - Limits: &swarm.Resources{ - NanoCPUs: r.limitCPU.Value(), - MemoryBytes: r.limitMemBytes.Value(), - }, - Reservations: &swarm.Resources{ - NanoCPUs: r.resCPU.Value(), - MemoryBytes: r.resMemBytes.Value(), - }, - } -} - -type restartPolicyOptions struct { - condition string - delay DurationOpt - maxAttempts Uint64Opt - window DurationOpt -} - -func defaultRestartPolicy() *swarm.RestartPolicy { - defaultMaxAttempts := defaults.Service.Task.Restart.MaxAttempts - rp := &swarm.RestartPolicy{ - MaxAttempts: &defaultMaxAttempts, - } - - if defaults.Service.Task.Restart.Delay != nil { - defaultRestartDelay, _ := gogotypes.DurationFromProto(defaults.Service.Task.Restart.Delay) - rp.Delay = &defaultRestartDelay - } - if defaults.Service.Task.Restart.Window != nil { - defaultRestartWindow, _ := gogotypes.DurationFromProto(defaults.Service.Task.Restart.Window) - rp.Window = &defaultRestartWindow - } - rp.Condition = defaultRestartCondition() - - return rp -} - -func defaultRestartCondition() swarm.RestartPolicyCondition { - switch defaults.Service.Task.Restart.Condition { - case api.RestartOnNone: - return "none" - case api.RestartOnFailure: - return "on-failure" - case api.RestartOnAny: - return "any" - default: - return "" - } -} - -func defaultOrder(order api.UpdateConfig_UpdateOrder) string { - switch order { - case api.UpdateConfig_STOP_FIRST: - return "stop-first" - case api.UpdateConfig_START_FIRST: - return "start-first" - default: - return "" - } -} - -func (r *restartPolicyOptions) ToRestartPolicy(flags *pflag.FlagSet) *swarm.RestartPolicy { - if !anyChanged(flags, flagRestartDelay, flagRestartMaxAttempts, flagRestartWindow, flagRestartCondition) { - return nil - } - - restartPolicy := defaultRestartPolicy() - - if flags.Changed(flagRestartDelay) { - restartPolicy.Delay = r.delay.Value() - } - if flags.Changed(flagRestartCondition) { - restartPolicy.Condition = swarm.RestartPolicyCondition(r.condition) - } - if flags.Changed(flagRestartMaxAttempts) { - restartPolicy.MaxAttempts = r.maxAttempts.Value() - } - if flags.Changed(flagRestartWindow) { - restartPolicy.Window = r.window.Value() - } - - return restartPolicy -} - -type credentialSpecOpt struct { - value *swarm.CredentialSpec - source string -} - -func (c *credentialSpecOpt) Set(value string) error { - c.source = value - c.value = &swarm.CredentialSpec{} - switch { - case strings.HasPrefix(value, "file://"): - c.value.File = strings.TrimPrefix(value, "file://") - case strings.HasPrefix(value, "registry://"): - c.value.Registry = strings.TrimPrefix(value, "registry://") - default: - return errors.New("Invalid credential spec - value must be prefixed file:// or registry:// followed by a value") - } - - return nil -} - -func (c *credentialSpecOpt) Type() string { - return "credential-spec" -} - -func (c *credentialSpecOpt) String() string { - return c.source -} - -func (c *credentialSpecOpt) Value() *swarm.CredentialSpec { - return c.value -} - -func convertNetworks(ctx context.Context, apiClient client.NetworkAPIClient, networks []string) ([]swarm.NetworkAttachmentConfig, error) { - nets := []swarm.NetworkAttachmentConfig{} - for _, networkIDOrName := range networks { - network, err := apiClient.NetworkInspect(ctx, networkIDOrName, false) - if err != nil { - return nil, err - } - nets = append(nets, swarm.NetworkAttachmentConfig{Target: network.ID}) - } - sort.Sort(byNetworkTarget(nets)) - return nets, nil -} - -type endpointOptions struct { - mode string - publishPorts opts.PortOpt -} - -func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec { - return &swarm.EndpointSpec{ - Mode: swarm.ResolutionMode(strings.ToLower(e.mode)), - Ports: e.publishPorts.Value(), - } -} - -type logDriverOptions struct { - name string - opts opts.ListOpts -} - -func newLogDriverOptions() logDriverOptions { - return logDriverOptions{opts: opts.NewListOpts(opts.ValidateEnv)} -} - -func (ldo *logDriverOptions) toLogDriver() *swarm.Driver { - if ldo.name == "" { - return nil - } - - // set the log driver only if specified. - return &swarm.Driver{ - Name: ldo.name, - Options: runconfigopts.ConvertKVStringsToMap(ldo.opts.GetAll()), - } -} - -type healthCheckOptions struct { - cmd string - interval PositiveDurationOpt - timeout PositiveDurationOpt - retries int - startPeriod PositiveDurationOpt - noHealthcheck bool -} - -func (opts *healthCheckOptions) toHealthConfig() (*container.HealthConfig, error) { - var healthConfig *container.HealthConfig - haveHealthSettings := opts.cmd != "" || - opts.interval.Value() != nil || - opts.timeout.Value() != nil || - opts.retries != 0 - if opts.noHealthcheck { - if haveHealthSettings { - return nil, errors.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) - } - healthConfig = &container.HealthConfig{Test: []string{"NONE"}} - } else if haveHealthSettings { - var test []string - if opts.cmd != "" { - test = []string{"CMD-SHELL", opts.cmd} - } - var interval, timeout, startPeriod time.Duration - if ptr := opts.interval.Value(); ptr != nil { - interval = *ptr - } - if ptr := opts.timeout.Value(); ptr != nil { - timeout = *ptr - } - if ptr := opts.startPeriod.Value(); ptr != nil { - startPeriod = *ptr - } - healthConfig = &container.HealthConfig{ - Test: test, - Interval: interval, - Timeout: timeout, - Retries: opts.retries, - StartPeriod: startPeriod, - } - } - return healthConfig, nil -} - -// convertExtraHostsToSwarmHosts converts an array of extra hosts in cli -// : -// into a swarmkit host format: -// IP_address canonical_hostname [aliases...] -// This assumes input value (:) has already been validated -func convertExtraHostsToSwarmHosts(extraHosts []string) []string { - hosts := []string{} - for _, extraHost := range extraHosts { - parts := strings.SplitN(extraHost, ":", 2) - hosts = append(hosts, fmt.Sprintf("%s %s", parts[1], parts[0])) - } - return hosts -} - -type serviceOptions struct { - detach bool - quiet bool - - name string - labels opts.ListOpts - containerLabels opts.ListOpts - image string - entrypoint ShlexOpt - args []string - hostname string - env opts.ListOpts - envFile opts.ListOpts - workdir string - user string - groups opts.ListOpts - credentialSpec credentialSpecOpt - stopSignal string - tty bool - readOnly bool - mounts opts.MountOpt - dns opts.ListOpts - dnsSearch opts.ListOpts - dnsOption opts.ListOpts - hosts opts.ListOpts - - resources resourceOptions - stopGrace DurationOpt - - replicas Uint64Opt - mode string - - restartPolicy restartPolicyOptions - constraints opts.ListOpts - placementPrefs placementPrefOpts - update updateOptions - rollback updateOptions - networks opts.ListOpts - endpoint endpointOptions - - registryAuth bool - - logDriver logDriverOptions - - healthcheck healthCheckOptions - secrets opts.SecretOpt -} - -func newServiceOptions() *serviceOptions { - return &serviceOptions{ - labels: opts.NewListOpts(opts.ValidateEnv), - constraints: opts.NewListOpts(nil), - containerLabels: opts.NewListOpts(opts.ValidateEnv), - env: opts.NewListOpts(opts.ValidateEnv), - envFile: opts.NewListOpts(nil), - groups: opts.NewListOpts(nil), - logDriver: newLogDriverOptions(), - dns: opts.NewListOpts(opts.ValidateIPAddress), - dnsOption: opts.NewListOpts(nil), - dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), - hosts: opts.NewListOpts(opts.ValidateExtraHost), - networks: opts.NewListOpts(nil), - } -} - -func (opts *serviceOptions) ToServiceMode() (swarm.ServiceMode, error) { - serviceMode := swarm.ServiceMode{} - switch opts.mode { - case "global": - if opts.replicas.Value() != nil { - return serviceMode, errors.Errorf("replicas can only be used with replicated mode") - } - - serviceMode.Global = &swarm.GlobalService{} - case "replicated": - serviceMode.Replicated = &swarm.ReplicatedService{ - Replicas: opts.replicas.Value(), - } - default: - return serviceMode, errors.Errorf("Unknown mode: %s, only replicated and global supported", opts.mode) - } - return serviceMode, nil -} - -func (opts *serviceOptions) ToStopGracePeriod(flags *pflag.FlagSet) *time.Duration { - if flags.Changed(flagStopGracePeriod) { - return opts.stopGrace.Value() - } - return nil -} - -func (opts *serviceOptions) ToService(ctx context.Context, apiClient client.APIClient, flags *pflag.FlagSet) (swarm.ServiceSpec, error) { - var service swarm.ServiceSpec - - envVariables, err := runconfigopts.ReadKVStrings(opts.envFile.GetAll(), opts.env.GetAll()) - if err != nil { - return service, err - } - - currentEnv := make([]string, 0, len(envVariables)) - for _, env := range envVariables { // need to process each var, in order - k := strings.SplitN(env, "=", 2)[0] - for i, current := range currentEnv { // remove duplicates - if current == env { - continue // no update required, may hide this behind flag to preserve order of envVariables - } - if strings.HasPrefix(current, k+"=") { - currentEnv = append(currentEnv[:i], currentEnv[i+1:]...) - } - } - currentEnv = append(currentEnv, env) - } - - healthConfig, err := opts.healthcheck.toHealthConfig() - if err != nil { - return service, err - } - - serviceMode, err := opts.ToServiceMode() - if err != nil { - return service, err - } - - networks, err := convertNetworks(ctx, apiClient, opts.networks.GetAll()) - if err != nil { - return service, err - } - - service = swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: opts.name, - Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()), - }, - TaskTemplate: swarm.TaskSpec{ - ContainerSpec: swarm.ContainerSpec{ - Image: opts.image, - Args: opts.args, - Command: opts.entrypoint.Value(), - Env: currentEnv, - Hostname: opts.hostname, - Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()), - Dir: opts.workdir, - User: opts.user, - Groups: opts.groups.GetAll(), - StopSignal: opts.stopSignal, - TTY: opts.tty, - ReadOnly: opts.readOnly, - Mounts: opts.mounts.Value(), - DNSConfig: &swarm.DNSConfig{ - Nameservers: opts.dns.GetAll(), - Search: opts.dnsSearch.GetAll(), - Options: opts.dnsOption.GetAll(), - }, - Hosts: convertExtraHostsToSwarmHosts(opts.hosts.GetAll()), - StopGracePeriod: opts.ToStopGracePeriod(flags), - Secrets: nil, - Healthcheck: healthConfig, - }, - Networks: networks, - Resources: opts.resources.ToResourceRequirements(), - RestartPolicy: opts.restartPolicy.ToRestartPolicy(flags), - Placement: &swarm.Placement{ - Constraints: opts.constraints.GetAll(), - Preferences: opts.placementPrefs.prefs, - }, - LogDriver: opts.logDriver.toLogDriver(), - }, - Mode: serviceMode, - UpdateConfig: opts.update.updateConfig(flags), - RollbackConfig: opts.update.rollbackConfig(flags), - EndpointSpec: opts.endpoint.ToEndpointSpec(), - } - - if opts.credentialSpec.Value() != nil { - service.TaskTemplate.ContainerSpec.Privileges = &swarm.Privileges{ - CredentialSpec: opts.credentialSpec.Value(), - } - } - - return service, nil -} - -type flagDefaults map[string]interface{} - -func (fd flagDefaults) getUint64(flagName string) uint64 { - if val, ok := fd[flagName].(uint64); ok { - return val - } - return 0 -} - -func (fd flagDefaults) getString(flagName string) string { - if val, ok := fd[flagName].(string); ok { - return val - } - return "" -} - -func buildServiceDefaultFlagMapping() flagDefaults { - defaultFlagValues := make(map[string]interface{}) - - defaultFlagValues[flagStopGracePeriod], _ = gogotypes.DurationFromProto(defaults.Service.Task.GetContainer().StopGracePeriod) - defaultFlagValues[flagRestartCondition] = `"` + defaultRestartCondition() + `"` - defaultFlagValues[flagRestartDelay], _ = gogotypes.DurationFromProto(defaults.Service.Task.Restart.Delay) - - if defaults.Service.Task.Restart.MaxAttempts != 0 { - defaultFlagValues[flagRestartMaxAttempts] = defaults.Service.Task.Restart.MaxAttempts - } - - defaultRestartWindow, _ := gogotypes.DurationFromProto(defaults.Service.Task.Restart.Window) - if defaultRestartWindow != 0 { - defaultFlagValues[flagRestartWindow] = defaultRestartWindow - } - - defaultFlagValues[flagUpdateParallelism] = defaults.Service.Update.Parallelism - defaultFlagValues[flagUpdateDelay] = defaults.Service.Update.Delay - defaultFlagValues[flagUpdateMonitor], _ = gogotypes.DurationFromProto(defaults.Service.Update.Monitor) - defaultFlagValues[flagUpdateFailureAction] = `"` + strings.ToLower(api.UpdateConfig_FailureAction_name[int32(defaults.Service.Update.FailureAction)]) + `"` - defaultFlagValues[flagUpdateMaxFailureRatio] = defaults.Service.Update.MaxFailureRatio - defaultFlagValues[flagUpdateOrder] = `"` + defaultOrder(defaults.Service.Update.Order) + `"` - - defaultFlagValues[flagRollbackParallelism] = defaults.Service.Rollback.Parallelism - defaultFlagValues[flagRollbackDelay] = defaults.Service.Rollback.Delay - defaultFlagValues[flagRollbackMonitor], _ = gogotypes.DurationFromProto(defaults.Service.Rollback.Monitor) - defaultFlagValues[flagRollbackFailureAction] = `"` + strings.ToLower(api.UpdateConfig_FailureAction_name[int32(defaults.Service.Rollback.FailureAction)]) + `"` - defaultFlagValues[flagRollbackMaxFailureRatio] = defaults.Service.Rollback.MaxFailureRatio - defaultFlagValues[flagRollbackOrder] = `"` + defaultOrder(defaults.Service.Rollback.Order) + `"` - - defaultFlagValues[flagEndpointMode] = "vip" - - return defaultFlagValues -} - -// addServiceFlags adds all flags that are common to both `create` and `update`. -// Any flags that are not common are added separately in the individual command -func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions, defaultFlagValues flagDefaults) { - flagDesc := func(flagName string, desc string) string { - if defaultValue, ok := defaultFlagValues[flagName]; ok { - return fmt.Sprintf("%s (default %v)", desc, defaultValue) - } - return desc - } - - flags.BoolVarP(&opts.detach, "detach", "d", true, "Exit immediately instead of waiting for the service to converge") - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress progress output") - - flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container") - flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: [:])") - flags.Var(&opts.credentialSpec, flagCredentialSpec, "Credential spec for managed service account (Windows only)") - flags.SetAnnotation(flagCredentialSpec, "version", []string{"1.29"}) - flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname") - flags.SetAnnotation(flagHostname, "version", []string{"1.25"}) - flags.Var(&opts.entrypoint, flagEntrypoint, "Overwrite the default ENTRYPOINT of the image") - - flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs") - flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory") - flags.Var(&opts.resources.resCPU, flagReserveCPU, "Reserve CPUs") - flags.Var(&opts.resources.resMemBytes, flagReserveMemory, "Reserve Memory") - - flags.Var(&opts.stopGrace, flagStopGracePeriod, flagDesc(flagStopGracePeriod, "Time to wait before force killing a container (ns|us|ms|s|m|h)")) - flags.Var(&opts.replicas, flagReplicas, "Number of tasks") - - flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", flagDesc(flagRestartCondition, `Restart when condition is met ("none"|"on-failure"|"any")`)) - flags.Var(&opts.restartPolicy.delay, flagRestartDelay, flagDesc(flagRestartDelay, "Delay between restart attempts (ns|us|ms|s|m|h)")) - flags.Var(&opts.restartPolicy.maxAttempts, flagRestartMaxAttempts, flagDesc(flagRestartMaxAttempts, "Maximum number of restarts before giving up")) - - flags.Var(&opts.restartPolicy.window, flagRestartWindow, flagDesc(flagRestartWindow, "Window used to evaluate the restart policy (ns|us|ms|s|m|h)")) - - flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, defaultFlagValues.getUint64(flagUpdateParallelism), "Maximum number of tasks updated simultaneously (0 to update all at once)") - flags.DurationVar(&opts.update.delay, flagUpdateDelay, 0, flagDesc(flagUpdateDelay, "Delay between updates (ns|us|ms|s|m|h)")) - flags.DurationVar(&opts.update.monitor, flagUpdateMonitor, 0, flagDesc(flagUpdateMonitor, "Duration after each task update to monitor for failure (ns|us|ms|s|m|h)")) - flags.SetAnnotation(flagUpdateMonitor, "version", []string{"1.25"}) - flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "", flagDesc(flagUpdateFailureAction, `Action on update failure ("pause"|"continue"|"rollback")`)) - flags.Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, flagDesc(flagUpdateMaxFailureRatio, "Failure rate to tolerate during an update")) - flags.SetAnnotation(flagUpdateMaxFailureRatio, "version", []string{"1.25"}) - flags.StringVar(&opts.update.order, flagUpdateOrder, "", flagDesc(flagUpdateOrder, `Update order ("start-first"|"stop-first")`)) - flags.SetAnnotation(flagUpdateOrder, "version", []string{"1.29"}) - - flags.Uint64Var(&opts.rollback.parallelism, flagRollbackParallelism, defaultFlagValues.getUint64(flagRollbackParallelism), "Maximum number of tasks rolled back simultaneously (0 to roll back all at once)") - flags.SetAnnotation(flagRollbackParallelism, "version", []string{"1.28"}) - flags.DurationVar(&opts.rollback.delay, flagRollbackDelay, 0, flagDesc(flagRollbackDelay, "Delay between task rollbacks (ns|us|ms|s|m|h)")) - flags.SetAnnotation(flagRollbackDelay, "version", []string{"1.28"}) - flags.DurationVar(&opts.rollback.monitor, flagRollbackMonitor, 0, flagDesc(flagRollbackMonitor, "Duration after each task rollback to monitor for failure (ns|us|ms|s|m|h)")) - flags.SetAnnotation(flagRollbackMonitor, "version", []string{"1.28"}) - flags.StringVar(&opts.rollback.onFailure, flagRollbackFailureAction, "", flagDesc(flagRollbackFailureAction, `Action on rollback failure ("pause"|"continue")`)) - flags.SetAnnotation(flagRollbackFailureAction, "version", []string{"1.28"}) - flags.Var(&opts.rollback.maxFailureRatio, flagRollbackMaxFailureRatio, flagDesc(flagRollbackMaxFailureRatio, "Failure rate to tolerate during a rollback")) - flags.SetAnnotation(flagRollbackMaxFailureRatio, "version", []string{"1.28"}) - flags.StringVar(&opts.rollback.order, flagRollbackOrder, "", flagDesc(flagRollbackOrder, `Rollback order ("start-first"|"stop-first")`)) - flags.SetAnnotation(flagRollbackOrder, "version", []string{"1.29"}) - - flags.StringVar(&opts.endpoint.mode, flagEndpointMode, defaultFlagValues.getString(flagEndpointMode), "Endpoint mode (vip or dnsrr)") - - flags.BoolVar(&opts.registryAuth, flagRegistryAuth, false, "Send registry authentication details to swarm agents") - - flags.StringVar(&opts.logDriver.name, flagLogDriver, "", "Logging driver for service") - flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options") - - flags.StringVar(&opts.healthcheck.cmd, flagHealthCmd, "", "Command to run to check health") - flags.SetAnnotation(flagHealthCmd, "version", []string{"1.25"}) - flags.Var(&opts.healthcheck.interval, flagHealthInterval, "Time between running the check (ms|s|m|h)") - flags.SetAnnotation(flagHealthInterval, "version", []string{"1.25"}) - flags.Var(&opts.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run (ms|s|m|h)") - flags.SetAnnotation(flagHealthTimeout, "version", []string{"1.25"}) - flags.IntVar(&opts.healthcheck.retries, flagHealthRetries, 0, "Consecutive failures needed to report unhealthy") - flags.SetAnnotation(flagHealthRetries, "version", []string{"1.25"}) - flags.Var(&opts.healthcheck.startPeriod, flagHealthStartPeriod, "Start period for the container to initialize before counting retries towards unstable (ms|s|m|h)") - flags.SetAnnotation(flagHealthStartPeriod, "version", []string{"1.29"}) - flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") - flags.SetAnnotation(flagNoHealthcheck, "version", []string{"1.25"}) - - flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY") - flags.SetAnnotation(flagTTY, "version", []string{"1.25"}) - - flags.BoolVar(&opts.readOnly, flagReadOnly, false, "Mount the container's root filesystem as read only") - flags.SetAnnotation(flagReadOnly, "version", []string{"1.28"}) - - flags.StringVar(&opts.stopSignal, flagStopSignal, "", "Signal to stop the container") - flags.SetAnnotation(flagStopSignal, "version", []string{"1.28"}) -} - -const ( - flagCredentialSpec = "credential-spec" - flagPlacementPref = "placement-pref" - flagPlacementPrefAdd = "placement-pref-add" - flagPlacementPrefRemove = "placement-pref-rm" - flagConstraint = "constraint" - flagConstraintRemove = "constraint-rm" - flagConstraintAdd = "constraint-add" - flagContainerLabel = "container-label" - flagContainerLabelRemove = "container-label-rm" - flagContainerLabelAdd = "container-label-add" - flagDNS = "dns" - flagDNSRemove = "dns-rm" - flagDNSAdd = "dns-add" - flagDNSOption = "dns-option" - flagDNSOptionRemove = "dns-option-rm" - flagDNSOptionAdd = "dns-option-add" - flagDNSSearch = "dns-search" - flagDNSSearchRemove = "dns-search-rm" - flagDNSSearchAdd = "dns-search-add" - flagEndpointMode = "endpoint-mode" - flagEntrypoint = "entrypoint" - flagHost = "host" - flagHostAdd = "host-add" - flagHostRemove = "host-rm" - flagHostname = "hostname" - flagEnv = "env" - flagEnvFile = "env-file" - flagEnvRemove = "env-rm" - flagEnvAdd = "env-add" - flagGroup = "group" - flagGroupAdd = "group-add" - flagGroupRemove = "group-rm" - flagLabel = "label" - flagLabelRemove = "label-rm" - flagLabelAdd = "label-add" - flagLimitCPU = "limit-cpu" - flagLimitMemory = "limit-memory" - flagMode = "mode" - flagMount = "mount" - flagMountRemove = "mount-rm" - flagMountAdd = "mount-add" - flagName = "name" - flagNetwork = "network" - flagNetworkAdd = "network-add" - flagNetworkRemove = "network-rm" - flagPublish = "publish" - flagPublishRemove = "publish-rm" - flagPublishAdd = "publish-add" - flagReadOnly = "read-only" - flagReplicas = "replicas" - flagReserveCPU = "reserve-cpu" - flagReserveMemory = "reserve-memory" - flagRestartCondition = "restart-condition" - flagRestartDelay = "restart-delay" - flagRestartMaxAttempts = "restart-max-attempts" - flagRestartWindow = "restart-window" - flagRollbackDelay = "rollback-delay" - flagRollbackFailureAction = "rollback-failure-action" - flagRollbackMaxFailureRatio = "rollback-max-failure-ratio" - flagRollbackMonitor = "rollback-monitor" - flagRollbackOrder = "rollback-order" - flagRollbackParallelism = "rollback-parallelism" - flagStopGracePeriod = "stop-grace-period" - flagStopSignal = "stop-signal" - flagTTY = "tty" - flagUpdateDelay = "update-delay" - flagUpdateFailureAction = "update-failure-action" - flagUpdateMaxFailureRatio = "update-max-failure-ratio" - flagUpdateMonitor = "update-monitor" - flagUpdateOrder = "update-order" - flagUpdateParallelism = "update-parallelism" - flagUser = "user" - flagWorkdir = "workdir" - flagRegistryAuth = "with-registry-auth" - flagLogDriver = "log-driver" - flagLogOpt = "log-opt" - flagHealthCmd = "health-cmd" - flagHealthInterval = "health-interval" - flagHealthRetries = "health-retries" - flagHealthTimeout = "health-timeout" - flagHealthStartPeriod = "health-start-period" - flagNoHealthcheck = "no-healthcheck" - flagSecret = "secret" - flagSecretAdd = "secret-add" - flagSecretRemove = "secret-rm" -) diff --git a/cli/command/service/opts_test.go b/cli/command/service/opts_test.go deleted file mode 100644 index 675fbe4b99..0000000000 --- a/cli/command/service/opts_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package service - -import ( - "testing" - "time" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/opts" - "github.com/stretchr/testify/assert" -) - -func TestMemBytesString(t *testing.T) { - var mem opts.MemBytes = 1048576 - assert.Equal(t, "1MiB", mem.String()) -} - -func TestMemBytesSetAndValue(t *testing.T) { - var mem opts.MemBytes - assert.NoError(t, mem.Set("5kb")) - assert.Equal(t, int64(5120), mem.Value()) -} - -func TestNanoCPUsString(t *testing.T) { - var cpus opts.NanoCPUs = 6100000000 - assert.Equal(t, "6.100", cpus.String()) -} - -func TestNanoCPUsSetAndValue(t *testing.T) { - var cpus opts.NanoCPUs - assert.NoError(t, cpus.Set("0.35")) - assert.Equal(t, int64(350000000), cpus.Value()) -} - -func TestDurationOptString(t *testing.T) { - dur := time.Duration(300 * 10e8) - duration := DurationOpt{value: &dur} - assert.Equal(t, "5m0s", duration.String()) -} - -func TestDurationOptSetAndValue(t *testing.T) { - var duration DurationOpt - assert.NoError(t, duration.Set("300s")) - assert.Equal(t, time.Duration(300*10e8), *duration.Value()) - assert.NoError(t, duration.Set("-300s")) - assert.Equal(t, time.Duration(-300*10e8), *duration.Value()) -} - -func TestPositiveDurationOptSetAndValue(t *testing.T) { - var duration PositiveDurationOpt - assert.NoError(t, duration.Set("300s")) - assert.Equal(t, time.Duration(300*10e8), *duration.Value()) - assert.EqualError(t, duration.Set("-300s"), "duration cannot be negative") -} - -func TestUint64OptString(t *testing.T) { - value := uint64(2345678) - opt := Uint64Opt{value: &value} - assert.Equal(t, "2345678", opt.String()) - - opt = Uint64Opt{} - assert.Equal(t, "", opt.String()) -} - -func TestUint64OptSetAndValue(t *testing.T) { - var opt Uint64Opt - assert.NoError(t, opt.Set("14445")) - assert.Equal(t, uint64(14445), *opt.Value()) -} - -func TestHealthCheckOptionsToHealthConfig(t *testing.T) { - dur := time.Second - opt := healthCheckOptions{ - cmd: "curl", - interval: PositiveDurationOpt{DurationOpt{value: &dur}}, - timeout: PositiveDurationOpt{DurationOpt{value: &dur}}, - startPeriod: PositiveDurationOpt{DurationOpt{value: &dur}}, - retries: 10, - } - config, err := opt.toHealthConfig() - assert.NoError(t, err) - assert.Equal(t, &container.HealthConfig{ - Test: []string{"CMD-SHELL", "curl"}, - Interval: time.Second, - Timeout: time.Second, - StartPeriod: time.Second, - Retries: 10, - }, config) -} - -func TestHealthCheckOptionsToHealthConfigNoHealthcheck(t *testing.T) { - opt := healthCheckOptions{ - noHealthcheck: true, - } - config, err := opt.toHealthConfig() - assert.NoError(t, err) - assert.Equal(t, &container.HealthConfig{ - Test: []string{"NONE"}, - }, config) -} - -func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) { - opt := healthCheckOptions{ - cmd: "curl", - noHealthcheck: true, - } - _, err := opt.toHealthConfig() - assert.EqualError(t, err, "--no-healthcheck conflicts with --health-* options") -} diff --git a/cli/command/service/parse.go b/cli/command/service/parse.go deleted file mode 100644 index acee08761f..0000000000 --- a/cli/command/service/parse.go +++ /dev/null @@ -1,59 +0,0 @@ -package service - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - swarmtypes "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/pkg/errors" - "golang.org/x/net/context" -) - -// ParseSecrets retrieves the secrets with the requested names and fills -// secret IDs into the secret references. -func ParseSecrets(client client.SecretAPIClient, requestedSecrets []*swarmtypes.SecretReference) ([]*swarmtypes.SecretReference, error) { - secretRefs := make(map[string]*swarmtypes.SecretReference) - ctx := context.Background() - - for _, secret := range requestedSecrets { - if _, exists := secretRefs[secret.File.Name]; exists { - return nil, errors.Errorf("duplicate secret target for %s not allowed", secret.SecretName) - } - secretRef := new(swarmtypes.SecretReference) - *secretRef = *secret - secretRefs[secret.File.Name] = secretRef - } - - args := filters.NewArgs() - for _, s := range secretRefs { - args.Add("name", s.SecretName) - } - - secrets, err := client.SecretList(ctx, types.SecretListOptions{ - Filters: args, - }) - if err != nil { - return nil, err - } - - foundSecrets := make(map[string]string) - for _, secret := range secrets { - foundSecrets[secret.Spec.Annotations.Name] = secret.ID - } - - addedSecrets := []*swarmtypes.SecretReference{} - - for _, ref := range secretRefs { - id, ok := foundSecrets[ref.SecretName] - if !ok { - return nil, errors.Errorf("secret not found: %s", ref.SecretName) - } - - // set the id for the ref to properly assign in swarm - // since swarm needs the ID instead of the name - ref.SecretID = id - addedSecrets = append(addedSecrets, ref) - } - - return addedSecrets, nil -} diff --git a/cli/command/service/progress/progress.go b/cli/command/service/progress/progress.go deleted file mode 100644 index d68fc6c1af..0000000000 --- a/cli/command/service/progress/progress.go +++ /dev/null @@ -1,409 +0,0 @@ -package progress - -import ( - "errors" - "fmt" - "io" - "os" - "os/signal" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/progress" - "github.com/docker/docker/pkg/streamformatter" - "github.com/docker/docker/pkg/stringid" - "golang.org/x/net/context" -) - -var ( - numberedStates = map[swarm.TaskState]int64{ - swarm.TaskStateNew: 1, - swarm.TaskStateAllocated: 2, - swarm.TaskStatePending: 3, - swarm.TaskStateAssigned: 4, - swarm.TaskStateAccepted: 5, - swarm.TaskStatePreparing: 6, - swarm.TaskStateReady: 7, - swarm.TaskStateStarting: 8, - swarm.TaskStateRunning: 9, - } - - longestState int -) - -const ( - maxProgress = 9 - maxProgressBars = 20 -) - -type progressUpdater interface { - update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]swarm.Node, rollback bool) (bool, error) -} - -func init() { - for state := range numberedStates { - if len(state) > longestState { - longestState = len(state) - } - } -} - -func stateToProgress(state swarm.TaskState, rollback bool) int64 { - if !rollback { - return numberedStates[state] - } - return int64(len(numberedStates)) - numberedStates[state] -} - -// ServiceProgress outputs progress information for convergence of a service. -func ServiceProgress(ctx context.Context, client client.APIClient, serviceID string, progressWriter io.WriteCloser) error { - defer progressWriter.Close() - - progressOut := streamformatter.NewJSONProgressOutput(progressWriter, false) - - sigint := make(chan os.Signal, 1) - signal.Notify(sigint, os.Interrupt) - defer signal.Stop(sigint) - - taskFilter := filters.NewArgs() - taskFilter.Add("service", serviceID) - taskFilter.Add("_up-to-date", "true") - - getUpToDateTasks := func() ([]swarm.Task, error) { - return client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter}) - } - - var ( - updater progressUpdater - converged bool - convergedAt time.Time - monitor = 5 * time.Second - rollback bool - ) - - for { - service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{}) - if err != nil { - return err - } - - if service.Spec.UpdateConfig != nil && service.Spec.UpdateConfig.Monitor != 0 { - monitor = service.Spec.UpdateConfig.Monitor - } - - if updater == nil { - updater, err = initializeUpdater(service, progressOut) - if err != nil { - return err - } - } - - if service.UpdateStatus != nil { - switch service.UpdateStatus.State { - case swarm.UpdateStateUpdating: - rollback = false - case swarm.UpdateStateCompleted: - if !converged { - return nil - } - case swarm.UpdateStatePaused: - return fmt.Errorf("service update paused: %s", service.UpdateStatus.Message) - case swarm.UpdateStateRollbackStarted: - if !rollback && service.UpdateStatus.Message != "" { - progressOut.WriteProgress(progress.Progress{ - ID: "rollback", - Action: service.UpdateStatus.Message, - }) - } - rollback = true - case swarm.UpdateStateRollbackPaused: - return fmt.Errorf("service rollback paused: %s", service.UpdateStatus.Message) - case swarm.UpdateStateRollbackCompleted: - if !converged { - return fmt.Errorf("service rolled back: %s", service.UpdateStatus.Message) - } - } - } - if converged && time.Since(convergedAt) >= monitor { - return nil - } - - tasks, err := getUpToDateTasks() - if err != nil { - return err - } - - activeNodes, err := getActiveNodes(ctx, client) - if err != nil { - return err - } - - converged, err = updater.update(service, tasks, activeNodes, rollback) - if err != nil { - return err - } - if converged { - if convergedAt.IsZero() { - convergedAt = time.Now() - } - wait := monitor - time.Since(convergedAt) - if wait >= 0 { - progressOut.WriteProgress(progress.Progress{ - // Ideally this would have no ID, but - // the progress rendering code behaves - // poorly on an "action" with no ID. It - // returns the cursor to the beginning - // of the line, so the first character - // may be difficult to read. Then the - // output is overwritten by the shell - // prompt when the command finishes. - ID: "verify", - Action: fmt.Sprintf("Waiting %d seconds to verify that tasks are stable...", wait/time.Second+1), - }) - } - } else { - if !convergedAt.IsZero() { - progressOut.WriteProgress(progress.Progress{ - ID: "verify", - Action: "Detected task failure", - }) - } - convergedAt = time.Time{} - } - - select { - case <-time.After(200 * time.Millisecond): - case <-sigint: - if !converged { - progress.Message(progressOut, "", "Operation continuing in background.") - progress.Messagef(progressOut, "", "Use `docker service ps %s` to check progress.", serviceID) - } - return nil - } - } -} - -func getActiveNodes(ctx context.Context, client client.APIClient) (map[string]swarm.Node, error) { - nodes, err := client.NodeList(ctx, types.NodeListOptions{}) - if err != nil { - return nil, err - } - - activeNodes := make(map[string]swarm.Node) - for _, n := range nodes { - if n.Status.State != swarm.NodeStateDown { - activeNodes[n.ID] = n - } - } - return activeNodes, nil -} - -func initializeUpdater(service swarm.Service, progressOut progress.Output) (progressUpdater, error) { - if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { - return &replicatedProgressUpdater{ - progressOut: progressOut, - }, nil - } - if service.Spec.Mode.Global != nil { - return &globalProgressUpdater{ - progressOut: progressOut, - }, nil - } - return nil, errors.New("unrecognized service mode") -} - -func writeOverallProgress(progressOut progress.Output, numerator, denominator int, rollback bool) { - if rollback { - progressOut.WriteProgress(progress.Progress{ - ID: "overall progress", - Action: fmt.Sprintf("rolling back update: %d out of %d tasks", numerator, denominator), - }) - return - } - progressOut.WriteProgress(progress.Progress{ - ID: "overall progress", - Action: fmt.Sprintf("%d out of %d tasks", numerator, denominator), - }) -} - -type replicatedProgressUpdater struct { - progressOut progress.Output - - // used for maping slots to a contiguous space - // this also causes progress bars to appear in order - slotMap map[int]int - - initialized bool - done bool -} - -func (u *replicatedProgressUpdater) update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]swarm.Node, rollback bool) (bool, error) { - if service.Spec.Mode.Replicated == nil || service.Spec.Mode.Replicated.Replicas == nil { - return false, errors.New("no replica count") - } - replicas := *service.Spec.Mode.Replicated.Replicas - - if !u.initialized { - u.slotMap = make(map[int]int) - - // Draw progress bars in order - writeOverallProgress(u.progressOut, 0, int(replicas), rollback) - - if replicas <= maxProgressBars { - for i := uint64(1); i <= replicas; i++ { - progress.Update(u.progressOut, fmt.Sprintf("%d/%d", i, replicas), " ") - } - } - u.initialized = true - } - - // If there are multiple tasks with the same slot number, favor the one - // with the *lowest* desired state. This can happen in restart - // scenarios. - tasksBySlot := make(map[int]swarm.Task) - for _, task := range tasks { - if numberedStates[task.DesiredState] == 0 { - continue - } - if existingTask, ok := tasksBySlot[task.Slot]; ok { - if numberedStates[existingTask.DesiredState] <= numberedStates[task.DesiredState] { - continue - } - } - if _, nodeActive := activeNodes[task.NodeID]; nodeActive { - tasksBySlot[task.Slot] = task - } - } - - // If we had reached a converged state, check if we are still converged. - if u.done { - for _, task := range tasksBySlot { - if task.Status.State != swarm.TaskStateRunning { - u.done = false - break - } - } - } - - running := uint64(0) - - for _, task := range tasksBySlot { - mappedSlot := u.slotMap[task.Slot] - if mappedSlot == 0 { - mappedSlot = len(u.slotMap) + 1 - u.slotMap[task.Slot] = mappedSlot - } - - if !u.done && replicas <= maxProgressBars && uint64(mappedSlot) <= replicas { - u.progressOut.WriteProgress(progress.Progress{ - ID: fmt.Sprintf("%d/%d", mappedSlot, replicas), - Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State), - Current: stateToProgress(task.Status.State, rollback), - Total: maxProgress, - HideCounts: true, - }) - } - if task.Status.State == swarm.TaskStateRunning { - running++ - } - } - - if !u.done { - writeOverallProgress(u.progressOut, int(running), int(replicas), rollback) - - if running == replicas { - u.done = true - } - } - - return running == replicas, nil -} - -type globalProgressUpdater struct { - progressOut progress.Output - - initialized bool - done bool -} - -func (u *globalProgressUpdater) update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]swarm.Node, rollback bool) (bool, error) { - // If there are multiple tasks with the same node ID, favor the one - // with the *lowest* desired state. This can happen in restart - // scenarios. - tasksByNode := make(map[string]swarm.Task) - for _, task := range tasks { - if numberedStates[task.DesiredState] == 0 { - continue - } - if existingTask, ok := tasksByNode[task.NodeID]; ok { - if numberedStates[existingTask.DesiredState] <= numberedStates[task.DesiredState] { - continue - } - } - tasksByNode[task.NodeID] = task - } - - // We don't have perfect knowledge of how many nodes meet the - // constraints for this service. But the orchestrator creates tasks - // for all eligible nodes at the same time, so we should see all those - // nodes represented among the up-to-date tasks. - nodeCount := len(tasksByNode) - - if !u.initialized { - if nodeCount == 0 { - // Two possibilities: either the orchestrator hasn't created - // the tasks yet, or the service doesn't meet constraints for - // any node. Either way, we wait. - u.progressOut.WriteProgress(progress.Progress{ - ID: "overall progress", - Action: "waiting for new tasks", - }) - return false, nil - } - - writeOverallProgress(u.progressOut, 0, nodeCount, rollback) - u.initialized = true - } - - // If we had reached a converged state, check if we are still converged. - if u.done { - for _, task := range tasksByNode { - if task.Status.State != swarm.TaskStateRunning { - u.done = false - break - } - } - } - - running := 0 - - for _, task := range tasksByNode { - if node, nodeActive := activeNodes[task.NodeID]; nodeActive { - if !u.done && nodeCount <= maxProgressBars { - u.progressOut.WriteProgress(progress.Progress{ - ID: stringid.TruncateID(node.ID), - Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State), - Current: stateToProgress(task.Status.State, rollback), - Total: maxProgress, - HideCounts: true, - }) - } - if task.Status.State == swarm.TaskStateRunning { - running++ - } - } - } - - if !u.done { - writeOverallProgress(u.progressOut, running, nodeCount, rollback) - - if running == nodeCount { - u.done = true - } - } - - return running == nodeCount, nil -} diff --git a/cli/command/service/ps.go b/cli/command/service/ps.go deleted file mode 100644 index 3a53a545d0..0000000000 --- a/cli/command/service/ps.go +++ /dev/null @@ -1,123 +0,0 @@ -package service - -import ( - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/cli/command/idresolver" - "github.com/docker/docker/cli/command/node" - "github.com/docker/docker/cli/command/task" - "github.com/docker/docker/opts" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -type psOptions struct { - services []string - quiet bool - noResolve bool - noTrunc bool - format string - filter opts.FilterOpt -} - -func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := psOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ps [OPTIONS] SERVICE [SERVICE...]", - Short: "List the tasks of one or more services", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.services = args - return runPS(dockerCli, opts) - }, - } - flags := cmd.Flags() - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs") - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") - flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") - flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - - return cmd -} - -func runPS(dockerCli *command.DockerCli, opts psOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - filter := opts.filter.Value() - - serviceIDFilter := filters.NewArgs() - serviceNameFilter := filters.NewArgs() - for _, service := range opts.services { - serviceIDFilter.Add("id", service) - serviceNameFilter.Add("name", service) - } - serviceByIDList, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceIDFilter}) - if err != nil { - return err - } - serviceByNameList, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceNameFilter}) - if err != nil { - return err - } - - for _, service := range opts.services { - serviceCount := 0 - // Lookup by ID/Prefix - for _, serviceEntry := range serviceByIDList { - if strings.HasPrefix(serviceEntry.ID, service) { - filter.Add("service", serviceEntry.ID) - serviceCount++ - } - } - - // Lookup by Name/Prefix - for _, serviceEntry := range serviceByNameList { - if strings.HasPrefix(serviceEntry.Spec.Annotations.Name, service) { - filter.Add("service", serviceEntry.ID) - serviceCount++ - } - } - // If nothing has been found, return immediately. - if serviceCount == 0 { - return errors.Errorf("no such services: %s", service) - } - } - - if filter.Include("node") { - nodeFilters := filter.Get("node") - for _, nodeFilter := range nodeFilters { - nodeReference, err := node.Reference(ctx, client, nodeFilter) - if err != nil { - return err - } - filter.Del("node", nodeFilter) - filter.Add("node", nodeReference) - } - } - - tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter}) - if err != nil { - return err - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().TasksFormat - } else { - format = formatter.TableFormatKey - } - } - - return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format) -} diff --git a/cli/command/service/remove.go b/cli/command/service/remove.go deleted file mode 100644 index a7b0107089..0000000000 --- a/cli/command/service/remove.go +++ /dev/null @@ -1,48 +0,0 @@ -package service - -import ( - "fmt" - "strings" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { - - cmd := &cobra.Command{ - Use: "rm SERVICE [SERVICE...]", - Aliases: []string{"remove"}, - Short: "Remove one or more services", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(dockerCli, args) - }, - } - cmd.Flags() - - return cmd -} - -func runRemove(dockerCli *command.DockerCli, sids []string) error { - client := dockerCli.Client() - - ctx := context.Background() - - var errs []string - for _, sid := range sids { - err := client.ServiceRemove(ctx, sid) - if err != nil { - errs = append(errs, err.Error()) - continue - } - fmt.Fprintf(dockerCli.Out(), "%s\n", sid) - } - if len(errs) > 0 { - return errors.Errorf(strings.Join(errs, "\n")) - } - return nil -} diff --git a/cli/command/service/scale.go b/cli/command/service/scale.go deleted file mode 100644 index 98163c87c9..0000000000 --- a/cli/command/service/scale.go +++ /dev/null @@ -1,97 +0,0 @@ -package service - -import ( - "fmt" - "strconv" - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -func newScaleCommand(dockerCli *command.DockerCli) *cobra.Command { - return &cobra.Command{ - Use: "scale SERVICE=REPLICAS [SERVICE=REPLICAS...]", - Short: "Scale one or multiple replicated services", - Args: scaleArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runScale(dockerCli, args) - }, - } -} - -func scaleArgs(cmd *cobra.Command, args []string) error { - if err := cli.RequiresMinArgs(1)(cmd, args); err != nil { - return err - } - for _, arg := range args { - if parts := strings.SplitN(arg, "=", 2); len(parts) != 2 { - return errors.Errorf( - "Invalid scale specifier '%s'.\nSee '%s --help'.\n\nUsage: %s\n\n%s", - arg, - cmd.CommandPath(), - cmd.UseLine(), - cmd.Short, - ) - } - } - return nil -} - -func runScale(dockerCli *command.DockerCli, args []string) error { - var errs []string - for _, arg := range args { - parts := strings.SplitN(arg, "=", 2) - serviceID, scaleStr := parts[0], parts[1] - - // validate input arg scale number - scale, err := strconv.ParseUint(scaleStr, 10, 64) - if err != nil { - errs = append(errs, fmt.Sprintf("%s: invalid replicas value %s: %v", serviceID, scaleStr, err)) - continue - } - - if err := runServiceScale(dockerCli, serviceID, scale); err != nil { - errs = append(errs, fmt.Sprintf("%s: %v", serviceID, err)) - } - } - - if len(errs) == 0 { - return nil - } - return errors.Errorf(strings.Join(errs, "\n")) -} - -func runServiceScale(dockerCli *command.DockerCli, serviceID string, scale uint64) error { - client := dockerCli.Client() - ctx := context.Background() - - service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{}) - if err != nil { - return err - } - - serviceMode := &service.Spec.Mode - if serviceMode.Replicated == nil { - return errors.Errorf("scale can only be used with replicated mode") - } - - serviceMode.Replicated.Replicas = &scale - - response, err := client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec, types.ServiceUpdateOptions{}) - if err != nil { - return err - } - - for _, warning := range response.Warnings { - fmt.Fprintln(dockerCli.Err(), warning) - } - - fmt.Fprintf(dockerCli.Out(), "%s scaled to %d\n", serviceID, scale) - return nil -} diff --git a/cli/command/service/trust.go b/cli/command/service/trust.go deleted file mode 100644 index eba52a9dd1..0000000000 --- a/cli/command/service/trust.go +++ /dev/null @@ -1,87 +0,0 @@ -package service - -import ( - "encoding/hex" - - "github.com/Sirupsen/logrus" - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/trust" - "github.com/docker/docker/registry" - "github.com/docker/notary/tuf/data" - "github.com/opencontainers/go-digest" - "github.com/pkg/errors" - "golang.org/x/net/context" -) - -func resolveServiceImageDigest(dockerCli *command.DockerCli, service *swarm.ServiceSpec) error { - if !command.IsTrusted() { - // Digests are resolved by the daemon when not using content - // trust. - return nil - } - - ref, err := reference.ParseAnyReference(service.TaskTemplate.ContainerSpec.Image) - if err != nil { - return errors.Wrapf(err, "invalid reference %s", service.TaskTemplate.ContainerSpec.Image) - } - - // If reference does not have digest (is not canonical nor image id) - if _, ok := ref.(reference.Digested); !ok { - namedRef, ok := ref.(reference.Named) - if !ok { - return errors.New("failed to resolve image digest using content trust: reference is not named") - } - namedRef = reference.TagNameOnly(namedRef) - taggedRef, ok := namedRef.(reference.NamedTagged) - if !ok { - return errors.New("failed to resolve image digest using content trust: reference is not tagged") - } - - resolvedImage, err := trustedResolveDigest(context.Background(), dockerCli, taggedRef) - if err != nil { - return errors.Wrap(err, "failed to resolve image digest using content trust") - } - resolvedFamiliar := reference.FamiliarString(resolvedImage) - logrus.Debugf("resolved image tag to %s using content trust", resolvedFamiliar) - service.TaskTemplate.ContainerSpec.Image = resolvedFamiliar - } - - return nil -} - -func trustedResolveDigest(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (reference.Canonical, error) { - repoInfo, err := registry.ParseRepositoryInfo(ref) - if err != nil { - return nil, err - } - - authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) - - notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") - if err != nil { - return nil, errors.Wrap(err, "error establishing connection to trust repository") - } - - t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) - if err != nil { - return nil, trust.NotaryError(repoInfo.Name.Name(), err) - } - // Only get the tag if it's in the top level targets role or the releases delegation role - // ignore it if it's in any other delegation roles - if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { - return nil, trust.NotaryError(repoInfo.Name.Name(), errors.Errorf("No trust data for %s", reference.FamiliarString(ref))) - } - - logrus.Debugf("retrieving target for %s role\n", t.Role) - h, ok := t.Hashes["sha256"] - if !ok { - return nil, errors.New("no valid hash, expecting sha256") - } - - dgst := digest.NewDigestFromHex("sha256", hex.EncodeToString(h)) - - // Allow returning canonical reference with tag - return reference.WithDigest(ref, dgst) -} diff --git a/cli/command/service/update.go b/cli/command/service/update.go deleted file mode 100644 index 233da68eee..0000000000 --- a/cli/command/service/update.go +++ /dev/null @@ -1,1018 +0,0 @@ -package service - -import ( - "fmt" - "sort" - "strings" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - mounttypes "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/client" - "github.com/docker/docker/opts" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/docker/go-connections/nat" - "github.com/docker/swarmkit/api/defaults" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "golang.org/x/net/context" -) - -func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { - serviceOpts := newServiceOptions() - - cmd := &cobra.Command{ - Use: "update [OPTIONS] SERVICE", - Short: "Update a service", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return runUpdate(dockerCli, cmd.Flags(), serviceOpts, args[0]) - }, - } - - flags := cmd.Flags() - flags.String("image", "", "Service image tag") - flags.Var(&ShlexOpt{}, "args", "Service command args") - flags.Bool("rollback", false, "Rollback to previous specification") - flags.SetAnnotation("rollback", "version", []string{"1.25"}) - flags.Bool("force", false, "Force update even if no changes require it") - flags.SetAnnotation("force", "version", []string{"1.25"}) - addServiceFlags(flags, serviceOpts, nil) - - flags.Var(newListOptsVar(), flagEnvRemove, "Remove an environment variable") - flags.Var(newListOptsVar(), flagGroupRemove, "Remove a previously added supplementary user group from the container") - flags.SetAnnotation(flagGroupRemove, "version", []string{"1.25"}) - flags.Var(newListOptsVar(), flagLabelRemove, "Remove a label by its key") - flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key") - flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path") - // flags.Var(newListOptsVar().WithValidator(validatePublishRemove), flagPublishRemove, "Remove a published port by its target port") - flags.Var(&opts.PortOpt{}, flagPublishRemove, "Remove a published port by its target port") - flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint") - flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server") - flags.SetAnnotation(flagDNSRemove, "version", []string{"1.25"}) - flags.Var(newListOptsVar(), flagDNSOptionRemove, "Remove a DNS option") - flags.SetAnnotation(flagDNSOptionRemove, "version", []string{"1.25"}) - flags.Var(newListOptsVar(), flagDNSSearchRemove, "Remove a DNS search domain") - flags.SetAnnotation(flagDNSSearchRemove, "version", []string{"1.25"}) - flags.Var(newListOptsVar(), flagHostRemove, "Remove a custom host-to-IP mapping (host:ip)") - flags.SetAnnotation(flagHostRemove, "version", []string{"1.25"}) - flags.Var(&serviceOpts.labels, flagLabelAdd, "Add or update a service label") - flags.Var(&serviceOpts.containerLabels, flagContainerLabelAdd, "Add or update a container label") - flags.Var(&serviceOpts.env, flagEnvAdd, "Add or update an environment variable") - flags.Var(newListOptsVar(), flagSecretRemove, "Remove a secret") - flags.SetAnnotation(flagSecretRemove, "version", []string{"1.25"}) - flags.Var(&serviceOpts.secrets, flagSecretAdd, "Add or update a secret on a service") - flags.SetAnnotation(flagSecretAdd, "version", []string{"1.25"}) - flags.Var(&serviceOpts.mounts, flagMountAdd, "Add or update a mount on a service") - flags.Var(&serviceOpts.constraints, flagConstraintAdd, "Add or update a placement constraint") - flags.Var(&serviceOpts.placementPrefs, flagPlacementPrefAdd, "Add a placement preference") - flags.SetAnnotation(flagPlacementPrefAdd, "version", []string{"1.28"}) - flags.Var(&placementPrefOpts{}, flagPlacementPrefRemove, "Remove a placement preference") - flags.SetAnnotation(flagPlacementPrefRemove, "version", []string{"1.28"}) - flags.Var(&serviceOpts.networks, flagNetworkAdd, "Add a network") - flags.SetAnnotation(flagNetworkAdd, "version", []string{"1.29"}) - flags.Var(newListOptsVar(), flagNetworkRemove, "Remove a network") - flags.SetAnnotation(flagNetworkRemove, "version", []string{"1.29"}) - flags.Var(&serviceOpts.endpoint.publishPorts, flagPublishAdd, "Add or update a published port") - flags.Var(&serviceOpts.groups, flagGroupAdd, "Add an additional supplementary user group to the container") - flags.SetAnnotation(flagGroupAdd, "version", []string{"1.25"}) - flags.Var(&serviceOpts.dns, flagDNSAdd, "Add or update a custom DNS server") - flags.SetAnnotation(flagDNSAdd, "version", []string{"1.25"}) - flags.Var(&serviceOpts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option") - flags.SetAnnotation(flagDNSOptionAdd, "version", []string{"1.25"}) - flags.Var(&serviceOpts.dnsSearch, flagDNSSearchAdd, "Add or update a custom DNS search domain") - flags.SetAnnotation(flagDNSSearchAdd, "version", []string{"1.25"}) - flags.Var(&serviceOpts.hosts, flagHostAdd, "Add or update a custom host-to-IP mapping (host:ip)") - flags.SetAnnotation(flagHostAdd, "version", []string{"1.25"}) - - return cmd -} - -func newListOptsVar() *opts.ListOpts { - return opts.NewListOptsRef(&[]string{}, nil) -} - -func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *serviceOptions, serviceID string) error { - apiClient := dockerCli.Client() - ctx := context.Background() - - service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{}) - if err != nil { - return err - } - - rollback, err := flags.GetBool("rollback") - if err != nil { - return err - } - - // There are two ways to do user-requested rollback. The old way is - // client-side, but with a sufficiently recent daemon we prefer - // server-side, because it will honor the rollback parameters. - var ( - clientSideRollback bool - serverSideRollback bool - ) - - spec := &service.Spec - if rollback { - // Rollback can't be combined with other flags. - otherFlagsPassed := false - flags.VisitAll(func(f *pflag.Flag) { - if f.Name == "rollback" { - return - } - if flags.Changed(f.Name) { - otherFlagsPassed = true - } - }) - if otherFlagsPassed { - return errors.New("other flags may not be combined with --rollback") - } - - if versions.LessThan(dockerCli.Client().ClientVersion(), "1.28") { - clientSideRollback = true - spec = service.PreviousSpec - if spec == nil { - return errors.Errorf("service does not have a previous specification to roll back to") - } - } else { - serverSideRollback = true - } - } - - updateOpts := types.ServiceUpdateOptions{} - if serverSideRollback { - updateOpts.Rollback = "previous" - } - - err = updateService(ctx, apiClient, flags, spec) - if err != nil { - return err - } - - if flags.Changed("image") { - if err := resolveServiceImageDigest(dockerCli, spec); err != nil { - return err - } - } - - updatedSecrets, err := getUpdatedSecrets(apiClient, flags, spec.TaskTemplate.ContainerSpec.Secrets) - if err != nil { - return err - } - - spec.TaskTemplate.ContainerSpec.Secrets = updatedSecrets - - // only send auth if flag was set - sendAuth, err := flags.GetBool(flagRegistryAuth) - if err != nil { - return err - } - if sendAuth { - // Retrieve encoded auth token from the image reference - // This would be the old image if it didn't change in this update - image := spec.TaskTemplate.ContainerSpec.Image - encodedAuth, err := command.RetrieveAuthTokenFromImage(ctx, dockerCli, image) - if err != nil { - return err - } - updateOpts.EncodedRegistryAuth = encodedAuth - } else if clientSideRollback { - updateOpts.RegistryAuthFrom = types.RegistryAuthFromPreviousSpec - } else { - updateOpts.RegistryAuthFrom = types.RegistryAuthFromSpec - } - - response, err := apiClient.ServiceUpdate(ctx, service.ID, service.Version, *spec, updateOpts) - if err != nil { - return err - } - - for _, warning := range response.Warnings { - fmt.Fprintln(dockerCli.Err(), warning) - } - - fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID) - - if opts.detach { - if !flags.Changed("detach") { - fmt.Fprintln(dockerCli.Err(), "Since --detach=false was not specified, tasks will be updated in the background.\n"+ - "In a future release, --detach=false will become the default.") - } - return nil - } - - return waitOnService(ctx, dockerCli, serviceID, opts) -} - -func updateService(ctx context.Context, apiClient client.APIClient, flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { - updateString := func(flag string, field *string) { - if flags.Changed(flag) { - *field, _ = flags.GetString(flag) - } - } - - updateInt64Value := func(flag string, field *int64) { - if flags.Changed(flag) { - *field = flags.Lookup(flag).Value.(int64Value).Value() - } - } - - updateFloatValue := func(flag string, field *float32) { - if flags.Changed(flag) { - *field = flags.Lookup(flag).Value.(*floatValue).Value() - } - } - - updateDuration := func(flag string, field *time.Duration) { - if flags.Changed(flag) { - *field, _ = flags.GetDuration(flag) - } - } - - updateDurationOpt := func(flag string, field **time.Duration) { - if flags.Changed(flag) { - val := *flags.Lookup(flag).Value.(*DurationOpt).Value() - *field = &val - } - } - - updateUint64 := func(flag string, field *uint64) { - if flags.Changed(flag) { - *field, _ = flags.GetUint64(flag) - } - } - - updateUint64Opt := func(flag string, field **uint64) { - if flags.Changed(flag) { - val := *flags.Lookup(flag).Value.(*Uint64Opt).Value() - *field = &val - } - } - - cspec := &spec.TaskTemplate.ContainerSpec - task := &spec.TaskTemplate - - taskResources := func() *swarm.ResourceRequirements { - if task.Resources == nil { - task.Resources = &swarm.ResourceRequirements{} - } - return task.Resources - } - - updateLabels(flags, &spec.Labels) - updateContainerLabels(flags, &cspec.Labels) - updateString("image", &cspec.Image) - updateStringToSlice(flags, "args", &cspec.Args) - updateStringToSlice(flags, flagEntrypoint, &cspec.Command) - updateEnvironment(flags, &cspec.Env) - updateString(flagWorkdir, &cspec.Dir) - updateString(flagUser, &cspec.User) - updateString(flagHostname, &cspec.Hostname) - if err := updateMounts(flags, &cspec.Mounts); err != nil { - return err - } - - if flags.Changed(flagLimitCPU) || flags.Changed(flagLimitMemory) { - taskResources().Limits = &swarm.Resources{} - updateInt64Value(flagLimitCPU, &task.Resources.Limits.NanoCPUs) - updateInt64Value(flagLimitMemory, &task.Resources.Limits.MemoryBytes) - } - if flags.Changed(flagReserveCPU) || flags.Changed(flagReserveMemory) { - taskResources().Reservations = &swarm.Resources{} - updateInt64Value(flagReserveCPU, &task.Resources.Reservations.NanoCPUs) - updateInt64Value(flagReserveMemory, &task.Resources.Reservations.MemoryBytes) - } - - updateDurationOpt(flagStopGracePeriod, &cspec.StopGracePeriod) - - if anyChanged(flags, flagRestartCondition, flagRestartDelay, flagRestartMaxAttempts, flagRestartWindow) { - if task.RestartPolicy == nil { - task.RestartPolicy = defaultRestartPolicy() - } - if flags.Changed(flagRestartCondition) { - value, _ := flags.GetString(flagRestartCondition) - task.RestartPolicy.Condition = swarm.RestartPolicyCondition(value) - } - updateDurationOpt(flagRestartDelay, &task.RestartPolicy.Delay) - updateUint64Opt(flagRestartMaxAttempts, &task.RestartPolicy.MaxAttempts) - updateDurationOpt(flagRestartWindow, &task.RestartPolicy.Window) - } - - if anyChanged(flags, flagConstraintAdd, flagConstraintRemove) { - if task.Placement == nil { - task.Placement = &swarm.Placement{} - } - updatePlacementConstraints(flags, task.Placement) - } - - if anyChanged(flags, flagPlacementPrefAdd, flagPlacementPrefRemove) { - if task.Placement == nil { - task.Placement = &swarm.Placement{} - } - updatePlacementPreferences(flags, task.Placement) - } - - if anyChanged(flags, flagNetworkAdd, flagNetworkRemove) { - if err := updateNetworks(ctx, apiClient, flags, spec); err != nil { - return err - } - } - - if err := updateReplicas(flags, &spec.Mode); err != nil { - return err - } - - if anyChanged(flags, flagUpdateParallelism, flagUpdateDelay, flagUpdateMonitor, flagUpdateFailureAction, flagUpdateMaxFailureRatio, flagUpdateOrder) { - if spec.UpdateConfig == nil { - spec.UpdateConfig = updateConfigFromDefaults(defaults.Service.Update) - } - updateUint64(flagUpdateParallelism, &spec.UpdateConfig.Parallelism) - updateDuration(flagUpdateDelay, &spec.UpdateConfig.Delay) - updateDuration(flagUpdateMonitor, &spec.UpdateConfig.Monitor) - updateString(flagUpdateFailureAction, &spec.UpdateConfig.FailureAction) - updateFloatValue(flagUpdateMaxFailureRatio, &spec.UpdateConfig.MaxFailureRatio) - updateString(flagUpdateOrder, &spec.UpdateConfig.Order) - } - - if anyChanged(flags, flagRollbackParallelism, flagRollbackDelay, flagRollbackMonitor, flagRollbackFailureAction, flagRollbackMaxFailureRatio, flagRollbackOrder) { - if spec.RollbackConfig == nil { - spec.RollbackConfig = updateConfigFromDefaults(defaults.Service.Rollback) - } - updateUint64(flagRollbackParallelism, &spec.RollbackConfig.Parallelism) - updateDuration(flagRollbackDelay, &spec.RollbackConfig.Delay) - updateDuration(flagRollbackMonitor, &spec.RollbackConfig.Monitor) - updateString(flagRollbackFailureAction, &spec.RollbackConfig.FailureAction) - updateFloatValue(flagRollbackMaxFailureRatio, &spec.RollbackConfig.MaxFailureRatio) - updateString(flagRollbackOrder, &spec.RollbackConfig.Order) - } - - if flags.Changed(flagEndpointMode) { - value, _ := flags.GetString(flagEndpointMode) - if spec.EndpointSpec == nil { - spec.EndpointSpec = &swarm.EndpointSpec{} - } - spec.EndpointSpec.Mode = swarm.ResolutionMode(value) - } - - if anyChanged(flags, flagGroupAdd, flagGroupRemove) { - if err := updateGroups(flags, &cspec.Groups); err != nil { - return err - } - } - - if anyChanged(flags, flagPublishAdd, flagPublishRemove) { - if spec.EndpointSpec == nil { - spec.EndpointSpec = &swarm.EndpointSpec{} - } - if err := updatePorts(flags, &spec.EndpointSpec.Ports); err != nil { - return err - } - } - - if anyChanged(flags, flagDNSAdd, flagDNSRemove, flagDNSOptionAdd, flagDNSOptionRemove, flagDNSSearchAdd, flagDNSSearchRemove) { - if cspec.DNSConfig == nil { - cspec.DNSConfig = &swarm.DNSConfig{} - } - if err := updateDNSConfig(flags, &cspec.DNSConfig); err != nil { - return err - } - } - - if anyChanged(flags, flagHostAdd, flagHostRemove) { - if err := updateHosts(flags, &cspec.Hosts); err != nil { - return err - } - } - - if err := updateLogDriver(flags, &spec.TaskTemplate); err != nil { - return err - } - - force, err := flags.GetBool("force") - if err != nil { - return err - } - - if force { - spec.TaskTemplate.ForceUpdate++ - } - - if err := updateHealthcheck(flags, cspec); err != nil { - return err - } - - if flags.Changed(flagTTY) { - tty, err := flags.GetBool(flagTTY) - if err != nil { - return err - } - cspec.TTY = tty - } - - if flags.Changed(flagReadOnly) { - readOnly, err := flags.GetBool(flagReadOnly) - if err != nil { - return err - } - cspec.ReadOnly = readOnly - } - - updateString(flagStopSignal, &cspec.StopSignal) - - return nil -} - -func updateStringToSlice(flags *pflag.FlagSet, flag string, field *[]string) { - if !flags.Changed(flag) { - return - } - - *field = flags.Lookup(flag).Value.(*ShlexOpt).Value() -} - -func anyChanged(flags *pflag.FlagSet, fields ...string) bool { - for _, flag := range fields { - if flags.Changed(flag) { - return true - } - } - return false -} - -func updatePlacementConstraints(flags *pflag.FlagSet, placement *swarm.Placement) { - if flags.Changed(flagConstraintAdd) { - values := flags.Lookup(flagConstraintAdd).Value.(*opts.ListOpts).GetAll() - placement.Constraints = append(placement.Constraints, values...) - } - toRemove := buildToRemoveSet(flags, flagConstraintRemove) - - newConstraints := []string{} - for _, constraint := range placement.Constraints { - if _, exists := toRemove[constraint]; !exists { - newConstraints = append(newConstraints, constraint) - } - } - // Sort so that result is predictable. - sort.Strings(newConstraints) - - placement.Constraints = newConstraints -} - -func updatePlacementPreferences(flags *pflag.FlagSet, placement *swarm.Placement) { - var newPrefs []swarm.PlacementPreference - - if flags.Changed(flagPlacementPrefRemove) { - for _, existing := range placement.Preferences { - removed := false - for _, removal := range flags.Lookup(flagPlacementPrefRemove).Value.(*placementPrefOpts).prefs { - if removal.Spread != nil && existing.Spread != nil && removal.Spread.SpreadDescriptor == existing.Spread.SpreadDescriptor { - removed = true - break - } - } - if !removed { - newPrefs = append(newPrefs, existing) - } - } - } else { - newPrefs = placement.Preferences - } - - if flags.Changed(flagPlacementPrefAdd) { - for _, addition := range flags.Lookup(flagPlacementPrefAdd).Value.(*placementPrefOpts).prefs { - newPrefs = append(newPrefs, addition) - } - } - - placement.Preferences = newPrefs -} - -func updateContainerLabels(flags *pflag.FlagSet, field *map[string]string) { - if flags.Changed(flagContainerLabelAdd) { - if *field == nil { - *field = map[string]string{} - } - - values := flags.Lookup(flagContainerLabelAdd).Value.(*opts.ListOpts).GetAll() - for key, value := range runconfigopts.ConvertKVStringsToMap(values) { - (*field)[key] = value - } - } - - if *field != nil && flags.Changed(flagContainerLabelRemove) { - toRemove := flags.Lookup(flagContainerLabelRemove).Value.(*opts.ListOpts).GetAll() - for _, label := range toRemove { - delete(*field, label) - } - } -} - -func updateLabels(flags *pflag.FlagSet, field *map[string]string) { - if flags.Changed(flagLabelAdd) { - if *field == nil { - *field = map[string]string{} - } - - values := flags.Lookup(flagLabelAdd).Value.(*opts.ListOpts).GetAll() - for key, value := range runconfigopts.ConvertKVStringsToMap(values) { - (*field)[key] = value - } - } - - if *field != nil && flags.Changed(flagLabelRemove) { - toRemove := flags.Lookup(flagLabelRemove).Value.(*opts.ListOpts).GetAll() - for _, label := range toRemove { - delete(*field, label) - } - } -} - -func updateEnvironment(flags *pflag.FlagSet, field *[]string) { - if flags.Changed(flagEnvAdd) { - envSet := map[string]string{} - for _, v := range *field { - envSet[envKey(v)] = v - } - - value := flags.Lookup(flagEnvAdd).Value.(*opts.ListOpts) - for _, v := range value.GetAll() { - envSet[envKey(v)] = v - } - - *field = []string{} - for _, v := range envSet { - *field = append(*field, v) - } - } - - toRemove := buildToRemoveSet(flags, flagEnvRemove) - *field = removeItems(*field, toRemove, envKey) -} - -func getUpdatedSecrets(apiClient client.SecretAPIClient, flags *pflag.FlagSet, secrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) { - newSecrets := []*swarm.SecretReference{} - - toRemove := buildToRemoveSet(flags, flagSecretRemove) - for _, secret := range secrets { - if _, exists := toRemove[secret.SecretName]; !exists { - newSecrets = append(newSecrets, secret) - } - } - - if flags.Changed(flagSecretAdd) { - values := flags.Lookup(flagSecretAdd).Value.(*opts.SecretOpt).Value() - - addSecrets, err := ParseSecrets(apiClient, values) - if err != nil { - return nil, err - } - newSecrets = append(newSecrets, addSecrets...) - } - - return newSecrets, nil -} - -func envKey(value string) string { - kv := strings.SplitN(value, "=", 2) - return kv[0] -} - -func itemKey(value string) string { - return value -} - -func buildToRemoveSet(flags *pflag.FlagSet, flag string) map[string]struct{} { - var empty struct{} - toRemove := make(map[string]struct{}) - - if !flags.Changed(flag) { - return toRemove - } - - toRemoveSlice := flags.Lookup(flag).Value.(*opts.ListOpts).GetAll() - for _, key := range toRemoveSlice { - toRemove[key] = empty - } - return toRemove -} - -func removeItems( - seq []string, - toRemove map[string]struct{}, - keyFunc func(string) string, -) []string { - newSeq := []string{} - for _, item := range seq { - if _, exists := toRemove[keyFunc(item)]; !exists { - newSeq = append(newSeq, item) - } - } - return newSeq -} - -type byMountSource []mounttypes.Mount - -func (m byMountSource) Len() int { return len(m) } -func (m byMountSource) Swap(i, j int) { m[i], m[j] = m[j], m[i] } -func (m byMountSource) Less(i, j int) bool { - a, b := m[i], m[j] - - if a.Source == b.Source { - return a.Target < b.Target - } - - return a.Source < b.Source -} - -func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) error { - mountsByTarget := map[string]mounttypes.Mount{} - - if flags.Changed(flagMountAdd) { - values := flags.Lookup(flagMountAdd).Value.(*opts.MountOpt).Value() - for _, mount := range values { - if _, ok := mountsByTarget[mount.Target]; ok { - return errors.Errorf("duplicate mount target") - } - mountsByTarget[mount.Target] = mount - } - } - - // Add old list of mount points minus updated one. - for _, mount := range *mounts { - if _, ok := mountsByTarget[mount.Target]; !ok { - mountsByTarget[mount.Target] = mount - } - } - - newMounts := []mounttypes.Mount{} - - toRemove := buildToRemoveSet(flags, flagMountRemove) - - for _, mount := range mountsByTarget { - if _, exists := toRemove[mount.Target]; !exists { - newMounts = append(newMounts, mount) - } - } - sort.Sort(byMountSource(newMounts)) - *mounts = newMounts - return nil -} - -func updateGroups(flags *pflag.FlagSet, groups *[]string) error { - if flags.Changed(flagGroupAdd) { - values := flags.Lookup(flagGroupAdd).Value.(*opts.ListOpts).GetAll() - *groups = append(*groups, values...) - } - toRemove := buildToRemoveSet(flags, flagGroupRemove) - - newGroups := []string{} - for _, group := range *groups { - if _, exists := toRemove[group]; !exists { - newGroups = append(newGroups, group) - } - } - // Sort so that result is predictable. - sort.Strings(newGroups) - - *groups = newGroups - return nil -} - -func removeDuplicates(entries []string) []string { - hit := map[string]bool{} - newEntries := []string{} - for _, v := range entries { - if !hit[v] { - newEntries = append(newEntries, v) - hit[v] = true - } - } - return newEntries -} - -func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error { - newConfig := &swarm.DNSConfig{} - - nameservers := (*config).Nameservers - if flags.Changed(flagDNSAdd) { - values := flags.Lookup(flagDNSAdd).Value.(*opts.ListOpts).GetAll() - nameservers = append(nameservers, values...) - } - nameservers = removeDuplicates(nameservers) - toRemove := buildToRemoveSet(flags, flagDNSRemove) - for _, nameserver := range nameservers { - if _, exists := toRemove[nameserver]; !exists { - newConfig.Nameservers = append(newConfig.Nameservers, nameserver) - - } - } - // Sort so that result is predictable. - sort.Strings(newConfig.Nameservers) - - search := (*config).Search - if flags.Changed(flagDNSSearchAdd) { - values := flags.Lookup(flagDNSSearchAdd).Value.(*opts.ListOpts).GetAll() - search = append(search, values...) - } - search = removeDuplicates(search) - toRemove = buildToRemoveSet(flags, flagDNSSearchRemove) - for _, entry := range search { - if _, exists := toRemove[entry]; !exists { - newConfig.Search = append(newConfig.Search, entry) - } - } - // Sort so that result is predictable. - sort.Strings(newConfig.Search) - - options := (*config).Options - if flags.Changed(flagDNSOptionAdd) { - values := flags.Lookup(flagDNSOptionAdd).Value.(*opts.ListOpts).GetAll() - options = append(options, values...) - } - options = removeDuplicates(options) - toRemove = buildToRemoveSet(flags, flagDNSOptionRemove) - for _, option := range options { - if _, exists := toRemove[option]; !exists { - newConfig.Options = append(newConfig.Options, option) - } - } - // Sort so that result is predictable. - sort.Strings(newConfig.Options) - - *config = newConfig - return nil -} - -type byPortConfig []swarm.PortConfig - -func (r byPortConfig) Len() int { return len(r) } -func (r byPortConfig) Swap(i, j int) { r[i], r[j] = r[j], r[i] } -func (r byPortConfig) Less(i, j int) bool { - // We convert PortConfig into `port/protocol`, e.g., `80/tcp` - // In updatePorts we already filter out with map so there is duplicate entries - return portConfigToString(&r[i]) < portConfigToString(&r[j]) -} - -func portConfigToString(portConfig *swarm.PortConfig) string { - protocol := portConfig.Protocol - mode := portConfig.PublishMode - return fmt.Sprintf("%v:%v/%s/%s", portConfig.PublishedPort, portConfig.TargetPort, protocol, mode) -} - -func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error { - // The key of the map is `port/protocol`, e.g., `80/tcp` - portSet := map[string]swarm.PortConfig{} - - // Build the current list of portConfig - for _, entry := range *portConfig { - if _, ok := portSet[portConfigToString(&entry)]; !ok { - portSet[portConfigToString(&entry)] = entry - } - } - - newPorts := []swarm.PortConfig{} - - // Clean current ports - toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.PortOpt).Value() -portLoop: - for _, port := range portSet { - for _, pConfig := range toRemove { - if equalProtocol(port.Protocol, pConfig.Protocol) && - port.TargetPort == pConfig.TargetPort && - equalPublishMode(port.PublishMode, pConfig.PublishMode) { - continue portLoop - } - } - - newPorts = append(newPorts, port) - } - - // Check to see if there are any conflict in flags. - if flags.Changed(flagPublishAdd) { - ports := flags.Lookup(flagPublishAdd).Value.(*opts.PortOpt).Value() - - for _, port := range ports { - if _, ok := portSet[portConfigToString(&port)]; ok { - continue - } - //portSet[portConfigToString(&port)] = port - newPorts = append(newPorts, port) - } - } - - // Sort the PortConfig to avoid unnecessary updates - sort.Sort(byPortConfig(newPorts)) - *portConfig = newPorts - return nil -} - -func equalProtocol(prot1, prot2 swarm.PortConfigProtocol) bool { - return prot1 == prot2 || - (prot1 == swarm.PortConfigProtocol("") && prot2 == swarm.PortConfigProtocolTCP) || - (prot2 == swarm.PortConfigProtocol("") && prot1 == swarm.PortConfigProtocolTCP) -} - -func equalPublishMode(mode1, mode2 swarm.PortConfigPublishMode) bool { - return mode1 == mode2 || - (mode1 == swarm.PortConfigPublishMode("") && mode2 == swarm.PortConfigPublishModeIngress) || - (mode2 == swarm.PortConfigPublishMode("") && mode1 == swarm.PortConfigPublishModeIngress) -} - -func equalPort(targetPort nat.Port, port swarm.PortConfig) bool { - return (string(port.Protocol) == targetPort.Proto() && - port.TargetPort == uint32(targetPort.Int())) -} - -func updateReplicas(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error { - if !flags.Changed(flagReplicas) { - return nil - } - - if serviceMode == nil || serviceMode.Replicated == nil { - return errors.Errorf("replicas can only be used with replicated mode") - } - serviceMode.Replicated.Replicas = flags.Lookup(flagReplicas).Value.(*Uint64Opt).Value() - return nil -} - -func updateHosts(flags *pflag.FlagSet, hosts *[]string) error { - // Combine existing Hosts (in swarmkit format) with the host to add (convert to swarmkit format) - if flags.Changed(flagHostAdd) { - values := convertExtraHostsToSwarmHosts(flags.Lookup(flagHostAdd).Value.(*opts.ListOpts).GetAll()) - *hosts = append(*hosts, values...) - } - // Remove duplicate - *hosts = removeDuplicates(*hosts) - - keysToRemove := make(map[string]struct{}) - if flags.Changed(flagHostRemove) { - var empty struct{} - extraHostsToRemove := flags.Lookup(flagHostRemove).Value.(*opts.ListOpts).GetAll() - for _, entry := range extraHostsToRemove { - key := strings.SplitN(entry, ":", 2)[0] - keysToRemove[key] = empty - } - } - - newHosts := []string{} - for _, entry := range *hosts { - // Since this is in swarmkit format, we need to find the key, which is canonical_hostname of: - // IP_address canonical_hostname [aliases...] - parts := strings.Fields(entry) - if len(parts) > 1 { - key := parts[1] - if _, exists := keysToRemove[key]; !exists { - newHosts = append(newHosts, entry) - } - } else { - newHosts = append(newHosts, entry) - } - } - - // Sort so that result is predictable. - sort.Strings(newHosts) - - *hosts = newHosts - return nil -} - -// updateLogDriver updates the log driver only if the log driver flag is set. -// All options will be replaced with those provided on the command line. -func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error { - if !flags.Changed(flagLogDriver) { - return nil - } - - name, err := flags.GetString(flagLogDriver) - if err != nil { - return err - } - - if name == "" { - return nil - } - - taskTemplate.LogDriver = &swarm.Driver{ - Name: name, - Options: runconfigopts.ConvertKVStringsToMap(flags.Lookup(flagLogOpt).Value.(*opts.ListOpts).GetAll()), - } - - return nil -} - -func updateHealthcheck(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec) error { - if !anyChanged(flags, flagNoHealthcheck, flagHealthCmd, flagHealthInterval, flagHealthRetries, flagHealthTimeout, flagHealthStartPeriod) { - return nil - } - if containerSpec.Healthcheck == nil { - containerSpec.Healthcheck = &container.HealthConfig{} - } - noHealthcheck, err := flags.GetBool(flagNoHealthcheck) - if err != nil { - return err - } - if noHealthcheck { - if !anyChanged(flags, flagHealthCmd, flagHealthInterval, flagHealthRetries, flagHealthTimeout, flagHealthStartPeriod) { - containerSpec.Healthcheck = &container.HealthConfig{ - Test: []string{"NONE"}, - } - return nil - } - return errors.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) - } - if len(containerSpec.Healthcheck.Test) > 0 && containerSpec.Healthcheck.Test[0] == "NONE" { - containerSpec.Healthcheck.Test = nil - } - if flags.Changed(flagHealthInterval) { - val := *flags.Lookup(flagHealthInterval).Value.(*PositiveDurationOpt).Value() - containerSpec.Healthcheck.Interval = val - } - if flags.Changed(flagHealthTimeout) { - val := *flags.Lookup(flagHealthTimeout).Value.(*PositiveDurationOpt).Value() - containerSpec.Healthcheck.Timeout = val - } - if flags.Changed(flagHealthStartPeriod) { - val := *flags.Lookup(flagHealthStartPeriod).Value.(*PositiveDurationOpt).Value() - containerSpec.Healthcheck.StartPeriod = val - } - if flags.Changed(flagHealthRetries) { - containerSpec.Healthcheck.Retries, _ = flags.GetInt(flagHealthRetries) - } - if flags.Changed(flagHealthCmd) { - cmd, _ := flags.GetString(flagHealthCmd) - if cmd != "" { - containerSpec.Healthcheck.Test = []string{"CMD-SHELL", cmd} - } else { - containerSpec.Healthcheck.Test = nil - } - } - return nil -} - -type byNetworkTarget []swarm.NetworkAttachmentConfig - -func (m byNetworkTarget) Len() int { return len(m) } -func (m byNetworkTarget) Swap(i, j int) { m[i], m[j] = m[j], m[i] } -func (m byNetworkTarget) Less(i, j int) bool { - return m[i].Target < m[j].Target -} - -func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { - // spec.TaskTemplate.Networks takes precedence over the deprecated - // spec.Networks field. If spec.Network is in use, we'll migrate those - // values to spec.TaskTemplate.Networks. - specNetworks := spec.TaskTemplate.Networks - if len(specNetworks) == 0 { - specNetworks = spec.Networks - } - spec.Networks = nil - - toRemove := buildToRemoveSet(flags, flagNetworkRemove) - idsToRemove := make(map[string]struct{}) - for networkIDOrName := range toRemove { - network, err := apiClient.NetworkInspect(ctx, networkIDOrName, false) - if err != nil { - return err - } - idsToRemove[network.ID] = struct{}{} - } - - existingNetworks := make(map[string]struct{}) - var newNetworks []swarm.NetworkAttachmentConfig - for _, network := range specNetworks { - if _, exists := idsToRemove[network.Target]; exists { - continue - } - - newNetworks = append(newNetworks, network) - existingNetworks[network.Target] = struct{}{} - } - - if flags.Changed(flagNetworkAdd) { - values := flags.Lookup(flagNetworkAdd).Value.(*opts.ListOpts).GetAll() - networks, err := convertNetworks(ctx, apiClient, values) - if err != nil { - return err - } - for _, network := range networks { - if _, exists := existingNetworks[network.Target]; exists { - return errors.Errorf("service is already attached to network %s", network.Target) - } - newNetworks = append(newNetworks, network) - existingNetworks[network.Target] = struct{}{} - } - } - - sort.Sort(byNetworkTarget(newNetworks)) - - spec.TaskTemplate.Networks = newNetworks - return nil -} diff --git a/cli/command/service/update_test.go b/cli/command/service/update_test.go deleted file mode 100644 index 8f49d52ab8..0000000000 --- a/cli/command/service/update_test.go +++ /dev/null @@ -1,496 +0,0 @@ -package service - -import ( - "reflect" - "sort" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - mounttypes "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/swarm" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/net/context" -) - -func TestUpdateServiceArgs(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("args", "the \"new args\"") - - spec := &swarm.ServiceSpec{} - cspec := &spec.TaskTemplate.ContainerSpec - cspec.Args = []string{"old", "args"} - - updateService(nil, nil, flags, spec) - assert.Equal(t, []string{"the", "new args"}, cspec.Args) -} - -func TestUpdateLabels(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("label-add", "toadd=newlabel") - flags.Set("label-rm", "toremove") - - labels := map[string]string{ - "toremove": "thelabeltoremove", - "tokeep": "value", - } - - updateLabels(flags, &labels) - assert.Len(t, labels, 2) - assert.Equal(t, "value", labels["tokeep"]) - assert.Equal(t, "newlabel", labels["toadd"]) -} - -func TestUpdateLabelsRemoveALabelThatDoesNotExist(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("label-rm", "dne") - - labels := map[string]string{"foo": "theoldlabel"} - updateLabels(flags, &labels) - assert.Len(t, labels, 1) -} - -func TestUpdatePlacementConstraints(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("constraint-add", "node=toadd") - flags.Set("constraint-rm", "node!=toremove") - - placement := &swarm.Placement{ - Constraints: []string{"node!=toremove", "container=tokeep"}, - } - - updatePlacementConstraints(flags, placement) - require.Len(t, placement.Constraints, 2) - assert.Equal(t, "container=tokeep", placement.Constraints[0]) - assert.Equal(t, "node=toadd", placement.Constraints[1]) -} - -func TestUpdatePlacementPrefs(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("placement-pref-add", "spread=node.labels.dc") - flags.Set("placement-pref-rm", "spread=node.labels.rack") - - placement := &swarm.Placement{ - Preferences: []swarm.PlacementPreference{ - { - Spread: &swarm.SpreadOver{ - SpreadDescriptor: "node.labels.rack", - }, - }, - { - Spread: &swarm.SpreadOver{ - SpreadDescriptor: "node.labels.row", - }, - }, - }, - } - - updatePlacementPreferences(flags, placement) - require.Len(t, placement.Preferences, 2) - assert.Equal(t, "node.labels.row", placement.Preferences[0].Spread.SpreadDescriptor) - assert.Equal(t, "node.labels.dc", placement.Preferences[1].Spread.SpreadDescriptor) -} - -func TestUpdateEnvironment(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("env-add", "toadd=newenv") - flags.Set("env-rm", "toremove") - - envs := []string{"toremove=theenvtoremove", "tokeep=value"} - - updateEnvironment(flags, &envs) - require.Len(t, envs, 2) - // Order has been removed in updateEnvironment (map) - sort.Strings(envs) - assert.Equal(t, "toadd=newenv", envs[0]) - assert.Equal(t, "tokeep=value", envs[1]) -} - -func TestUpdateEnvironmentWithDuplicateValues(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("env-add", "foo=newenv") - flags.Set("env-add", "foo=dupe") - flags.Set("env-rm", "foo") - - envs := []string{"foo=value"} - - updateEnvironment(flags, &envs) - assert.Len(t, envs, 0) -} - -func TestUpdateEnvironmentWithDuplicateKeys(t *testing.T) { - // Test case for #25404 - flags := newUpdateCommand(nil).Flags() - flags.Set("env-add", "A=b") - - envs := []string{"A=c"} - - updateEnvironment(flags, &envs) - require.Len(t, envs, 1) - assert.Equal(t, "A=b", envs[0]) -} - -func TestUpdateGroups(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("group-add", "wheel") - flags.Set("group-add", "docker") - flags.Set("group-rm", "root") - flags.Set("group-add", "foo") - flags.Set("group-rm", "docker") - - groups := []string{"bar", "root"} - - updateGroups(flags, &groups) - require.Len(t, groups, 3) - assert.Equal(t, "bar", groups[0]) - assert.Equal(t, "foo", groups[1]) - assert.Equal(t, "wheel", groups[2]) -} - -func TestUpdateDNSConfig(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - - // IPv4, with duplicates - flags.Set("dns-add", "1.1.1.1") - flags.Set("dns-add", "1.1.1.1") - flags.Set("dns-add", "2.2.2.2") - flags.Set("dns-rm", "3.3.3.3") - flags.Set("dns-rm", "2.2.2.2") - // IPv6 - flags.Set("dns-add", "2001:db8:abc8::1") - // Invalid dns record - assert.EqualError(t, flags.Set("dns-add", "x.y.z.w"), "x.y.z.w is not an ip address") - - // domains with duplicates - flags.Set("dns-search-add", "example.com") - flags.Set("dns-search-add", "example.com") - flags.Set("dns-search-add", "example.org") - flags.Set("dns-search-rm", "example.org") - // Invalid dns search domain - assert.EqualError(t, flags.Set("dns-search-add", "example$com"), "example$com is not a valid domain") - - flags.Set("dns-option-add", "ndots:9") - flags.Set("dns-option-rm", "timeout:3") - - config := &swarm.DNSConfig{ - Nameservers: []string{"3.3.3.3", "5.5.5.5"}, - Search: []string{"localdomain"}, - Options: []string{"timeout:3"}, - } - - updateDNSConfig(flags, &config) - - require.Len(t, config.Nameservers, 3) - assert.Equal(t, "1.1.1.1", config.Nameservers[0]) - assert.Equal(t, "2001:db8:abc8::1", config.Nameservers[1]) - assert.Equal(t, "5.5.5.5", config.Nameservers[2]) - - require.Len(t, config.Search, 2) - assert.Equal(t, "example.com", config.Search[0]) - assert.Equal(t, "localdomain", config.Search[1]) - - require.Len(t, config.Options, 1) - assert.Equal(t, config.Options[0], "ndots:9") -} - -func TestUpdateMounts(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("mount-add", "type=volume,source=vol2,target=/toadd") - flags.Set("mount-rm", "/toremove") - - mounts := []mounttypes.Mount{ - {Target: "/toremove", Source: "vol1", Type: mounttypes.TypeBind}, - {Target: "/tokeep", Source: "vol3", Type: mounttypes.TypeBind}, - } - - updateMounts(flags, &mounts) - require.Len(t, mounts, 2) - assert.Equal(t, "/toadd", mounts[0].Target) - assert.Equal(t, "/tokeep", mounts[1].Target) -} - -func TestUpdateMountsWithDuplicateMounts(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("mount-add", "type=volume,source=vol4,target=/toadd") - - mounts := []mounttypes.Mount{ - {Target: "/tokeep1", Source: "vol1", Type: mounttypes.TypeBind}, - {Target: "/toadd", Source: "vol2", Type: mounttypes.TypeBind}, - {Target: "/tokeep2", Source: "vol3", Type: mounttypes.TypeBind}, - } - - updateMounts(flags, &mounts) - require.Len(t, mounts, 3) - assert.Equal(t, "/tokeep1", mounts[0].Target) - assert.Equal(t, "/tokeep2", mounts[1].Target) - assert.Equal(t, "/toadd", mounts[2].Target) -} - -func TestUpdatePorts(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("publish-add", "1000:1000") - flags.Set("publish-rm", "333/udp") - - portConfigs := []swarm.PortConfig{ - {TargetPort: 333, Protocol: swarm.PortConfigProtocolUDP}, - {TargetPort: 555}, - } - - err := updatePorts(flags, &portConfigs) - assert.NoError(t, err) - require.Len(t, portConfigs, 2) - // Do a sort to have the order (might have changed by map) - targetPorts := []int{int(portConfigs[0].TargetPort), int(portConfigs[1].TargetPort)} - sort.Ints(targetPorts) - assert.Equal(t, 555, targetPorts[0]) - assert.Equal(t, 1000, targetPorts[1]) -} - -func TestUpdatePortsDuplicate(t *testing.T) { - // Test case for #25375 - flags := newUpdateCommand(nil).Flags() - flags.Set("publish-add", "80:80") - - portConfigs := []swarm.PortConfig{ - { - TargetPort: 80, - PublishedPort: 80, - Protocol: swarm.PortConfigProtocolTCP, - PublishMode: swarm.PortConfigPublishModeIngress, - }, - } - - err := updatePorts(flags, &portConfigs) - assert.NoError(t, err) - require.Len(t, portConfigs, 1) - assert.Equal(t, uint32(80), portConfigs[0].TargetPort) -} - -func TestUpdateHealthcheckTable(t *testing.T) { - type test struct { - flags [][2]string - initial *container.HealthConfig - expected *container.HealthConfig - err string - } - testCases := []test{ - { - flags: [][2]string{{"no-healthcheck", "true"}}, - initial: &container.HealthConfig{Test: []string{"CMD-SHELL", "cmd1"}, Retries: 10}, - expected: &container.HealthConfig{Test: []string{"NONE"}}, - }, - { - flags: [][2]string{{"health-cmd", "cmd1"}}, - initial: &container.HealthConfig{Test: []string{"NONE"}}, - expected: &container.HealthConfig{Test: []string{"CMD-SHELL", "cmd1"}}, - }, - { - flags: [][2]string{{"health-retries", "10"}}, - initial: &container.HealthConfig{Test: []string{"NONE"}}, - expected: &container.HealthConfig{Retries: 10}, - }, - { - flags: [][2]string{{"health-retries", "10"}}, - initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}}, - expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10}, - }, - { - flags: [][2]string{{"health-interval", "1m"}}, - initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}}, - expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Interval: time.Minute}, - }, - { - flags: [][2]string{{"health-cmd", ""}}, - initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10}, - expected: &container.HealthConfig{Retries: 10}, - }, - { - flags: [][2]string{{"health-retries", "0"}}, - initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10}, - expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}}, - }, - { - flags: [][2]string{{"health-start-period", "1m"}}, - initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}}, - expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, StartPeriod: time.Minute}, - }, - { - flags: [][2]string{{"health-cmd", "cmd1"}, {"no-healthcheck", "true"}}, - err: "--no-healthcheck conflicts with --health-* options", - }, - { - flags: [][2]string{{"health-interval", "10m"}, {"no-healthcheck", "true"}}, - err: "--no-healthcheck conflicts with --health-* options", - }, - { - flags: [][2]string{{"health-timeout", "1m"}, {"no-healthcheck", "true"}}, - err: "--no-healthcheck conflicts with --health-* options", - }, - } - for i, c := range testCases { - flags := newUpdateCommand(nil).Flags() - for _, flag := range c.flags { - flags.Set(flag[0], flag[1]) - } - cspec := &swarm.ContainerSpec{ - Healthcheck: c.initial, - } - err := updateHealthcheck(flags, cspec) - if c.err != "" { - assert.EqualError(t, err, c.err) - } else { - assert.NoError(t, err) - if !reflect.DeepEqual(cspec.Healthcheck, c.expected) { - t.Errorf("incorrect result for test %d, expected health config:\n\t%#v\ngot:\n\t%#v", i, c.expected, cspec.Healthcheck) - } - } - } -} - -func TestUpdateHosts(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("host-add", "example.net:2.2.2.2") - flags.Set("host-add", "ipv6.net:2001:db8:abc8::1") - // remove with ipv6 should work - flags.Set("host-rm", "example.net:2001:db8:abc8::1") - // just hostname should work as well - flags.Set("host-rm", "example.net") - // bad format error - assert.EqualError(t, flags.Set("host-add", "$example.com$"), `bad format for add-host: "$example.com$"`) - - hosts := []string{"1.2.3.4 example.com", "4.3.2.1 example.org", "2001:db8:abc8::1 example.net"} - - updateHosts(flags, &hosts) - require.Len(t, hosts, 3) - assert.Equal(t, "1.2.3.4 example.com", hosts[0]) - assert.Equal(t, "2001:db8:abc8::1 ipv6.net", hosts[1]) - assert.Equal(t, "4.3.2.1 example.org", hosts[2]) -} - -func TestUpdatePortsRmWithProtocol(t *testing.T) { - flags := newUpdateCommand(nil).Flags() - flags.Set("publish-add", "8081:81") - flags.Set("publish-add", "8082:82") - flags.Set("publish-rm", "80") - flags.Set("publish-rm", "81/tcp") - flags.Set("publish-rm", "82/udp") - - portConfigs := []swarm.PortConfig{ - { - TargetPort: 80, - PublishedPort: 8080, - Protocol: swarm.PortConfigProtocolTCP, - PublishMode: swarm.PortConfigPublishModeIngress, - }, - } - - err := updatePorts(flags, &portConfigs) - assert.NoError(t, err) - require.Len(t, portConfigs, 2) - assert.Equal(t, uint32(81), portConfigs[0].TargetPort) - assert.Equal(t, uint32(82), portConfigs[1].TargetPort) -} - -type secretAPIClientMock struct { - listResult []swarm.Secret -} - -func (s secretAPIClientMock) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) { - return s.listResult, nil -} -func (s secretAPIClientMock) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) { - return types.SecretCreateResponse{}, nil -} -func (s secretAPIClientMock) SecretRemove(ctx context.Context, id string) error { - return nil -} -func (s secretAPIClientMock) SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error) { - return swarm.Secret{}, []byte{}, nil -} -func (s secretAPIClientMock) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error { - return nil -} - -// TestUpdateSecretUpdateInPlace tests the ability to update the "target" of an secret with "docker service update" -// by combining "--secret-rm" and "--secret-add" for the same secret. -func TestUpdateSecretUpdateInPlace(t *testing.T) { - apiClient := secretAPIClientMock{ - listResult: []swarm.Secret{ - { - ID: "tn9qiblgnuuut11eufquw5dev", - Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo"}}, - }, - }, - } - - flags := newUpdateCommand(nil).Flags() - flags.Set("secret-add", "source=foo,target=foo2") - flags.Set("secret-rm", "foo") - - secrets := []*swarm.SecretReference{ - { - File: &swarm.SecretReferenceFileTarget{ - Name: "foo", - UID: "0", - GID: "0", - Mode: 292, - }, - SecretID: "tn9qiblgnuuut11eufquw5dev", - SecretName: "foo", - }, - } - - updatedSecrets, err := getUpdatedSecrets(apiClient, flags, secrets) - - assert.NoError(t, err) - require.Len(t, updatedSecrets, 1) - assert.Equal(t, "tn9qiblgnuuut11eufquw5dev", updatedSecrets[0].SecretID) - assert.Equal(t, "foo", updatedSecrets[0].SecretName) - assert.Equal(t, "foo2", updatedSecrets[0].File.Name) -} - -func TestUpdateReadOnly(t *testing.T) { - spec := &swarm.ServiceSpec{} - cspec := &spec.TaskTemplate.ContainerSpec - - // Update with --read-only=true, changed to true - flags := newUpdateCommand(nil).Flags() - flags.Set("read-only", "true") - updateService(nil, nil, flags, spec) - assert.True(t, cspec.ReadOnly) - - // Update without --read-only, no change - flags = newUpdateCommand(nil).Flags() - updateService(nil, nil, flags, spec) - assert.True(t, cspec.ReadOnly) - - // Update with --read-only=false, changed to false - flags = newUpdateCommand(nil).Flags() - flags.Set("read-only", "false") - updateService(nil, nil, flags, spec) - assert.False(t, cspec.ReadOnly) -} - -func TestUpdateStopSignal(t *testing.T) { - spec := &swarm.ServiceSpec{} - cspec := &spec.TaskTemplate.ContainerSpec - - // Update with --stop-signal=SIGUSR1 - flags := newUpdateCommand(nil).Flags() - flags.Set("stop-signal", "SIGUSR1") - updateService(nil, nil, flags, spec) - assert.Equal(t, "SIGUSR1", cspec.StopSignal) - - // Update without --stop-signal, no change - flags = newUpdateCommand(nil).Flags() - updateService(nil, nil, flags, spec) - assert.Equal(t, "SIGUSR1", cspec.StopSignal) - - // Update with --stop-signal=SIGWINCH - flags = newUpdateCommand(nil).Flags() - flags.Set("stop-signal", "SIGWINCH") - updateService(nil, nil, flags, spec) - assert.Equal(t, "SIGWINCH", cspec.StopSignal) -} diff --git a/cli/command/stack/client_test.go b/cli/command/stack/client_test.go deleted file mode 100644 index 0cd8612b6d..0000000000 --- a/cli/command/stack/client_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package stack - -import ( - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/compose/convert" - "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -type fakeClient struct { - client.Client - - services []string - networks []string - secrets []string - - removedServices []string - removedNetworks []string - removedSecrets []string - - serviceListFunc func(options types.ServiceListOptions) ([]swarm.Service, error) - networkListFunc func(options types.NetworkListOptions) ([]types.NetworkResource, error) - secretListFunc func(options types.SecretListOptions) ([]swarm.Secret, error) - serviceRemoveFunc func(serviceID string) error - networkRemoveFunc func(networkID string) error - secretRemoveFunc func(secretID string) error -} - -func (cli *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) { - if cli.serviceListFunc != nil { - return cli.serviceListFunc(options) - } - - namespace := namespaceFromFilters(options.Filters) - servicesList := []swarm.Service{} - for _, name := range cli.services { - if belongToNamespace(name, namespace) { - servicesList = append(servicesList, serviceFromName(name)) - } - } - return servicesList, nil -} - -func (cli *fakeClient) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) { - if cli.networkListFunc != nil { - return cli.networkListFunc(options) - } - - namespace := namespaceFromFilters(options.Filters) - networksList := []types.NetworkResource{} - for _, name := range cli.networks { - if belongToNamespace(name, namespace) { - networksList = append(networksList, networkFromName(name)) - } - } - return networksList, nil -} - -func (cli *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) { - if cli.secretListFunc != nil { - return cli.secretListFunc(options) - } - - namespace := namespaceFromFilters(options.Filters) - secretsList := []swarm.Secret{} - for _, name := range cli.secrets { - if belongToNamespace(name, namespace) { - secretsList = append(secretsList, secretFromName(name)) - } - } - return secretsList, nil -} - -func (cli *fakeClient) ServiceRemove(ctx context.Context, serviceID string) error { - if cli.serviceRemoveFunc != nil { - return cli.serviceRemoveFunc(serviceID) - } - - cli.removedServices = append(cli.removedServices, serviceID) - return nil -} - -func (cli *fakeClient) NetworkRemove(ctx context.Context, networkID string) error { - if cli.networkRemoveFunc != nil { - return cli.networkRemoveFunc(networkID) - } - - cli.removedNetworks = append(cli.removedNetworks, networkID) - return nil -} - -func (cli *fakeClient) SecretRemove(ctx context.Context, secretID string) error { - if cli.secretRemoveFunc != nil { - return cli.secretRemoveFunc(secretID) - } - - cli.removedSecrets = append(cli.removedSecrets, secretID) - return nil -} - -func serviceFromName(name string) swarm.Service { - return swarm.Service{ - ID: "ID-" + name, - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: name}, - }, - } -} - -func networkFromName(name string) types.NetworkResource { - return types.NetworkResource{ - ID: "ID-" + name, - Name: name, - } -} - -func secretFromName(name string) swarm.Secret { - return swarm.Secret{ - ID: "ID-" + name, - Spec: swarm.SecretSpec{ - Annotations: swarm.Annotations{Name: name}, - }, - } -} - -func namespaceFromFilters(filters filters.Args) string { - label := filters.Get("label")[0] - return strings.TrimPrefix(label, convert.LabelNamespace+"=") -} - -func belongToNamespace(id, namespace string) bool { - return strings.HasPrefix(id, namespace+"_") -} - -func objectName(namespace, name string) string { - return namespace + "_" + name -} - -func objectID(name string) string { - return "ID-" + name -} - -func buildObjectIDs(objectNames []string) []string { - IDs := make([]string, len(objectNames)) - for i, name := range objectNames { - IDs[i] = objectID(name) - } - return IDs -} diff --git a/cli/command/stack/cmd.go b/cli/command/stack/cmd.go deleted file mode 100644 index 860bfedd1a..0000000000 --- a/cli/command/stack/cmd.go +++ /dev/null @@ -1,35 +0,0 @@ -package stack - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -// NewStackCommand returns a cobra command for `stack` subcommands -func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "stack", - Short: "Manage Docker stacks", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - Tags: map[string]string{"version": "1.25"}, - } - cmd.AddCommand( - newDeployCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - newServicesCommand(dockerCli), - newPsCommand(dockerCli), - ) - return cmd -} - -// NewTopLevelDeployCommand returns a command for `docker deploy` -func NewTopLevelDeployCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := newDeployCommand(dockerCli) - // Remove the aliases at the top level - cmd.Aliases = []string{} - cmd.Tags = map[string]string{"experimental": "", "version": "1.25"} - return cmd -} diff --git a/cli/command/stack/common.go b/cli/command/stack/common.go deleted file mode 100644 index f37d8aaa81..0000000000 --- a/cli/command/stack/common.go +++ /dev/null @@ -1,64 +0,0 @@ -package stack - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/compose/convert" - "github.com/docker/docker/client" - "github.com/docker/docker/opts" -) - -func getStackFilter(namespace string) filters.Args { - filter := filters.NewArgs() - filter.Add("label", convert.LabelNamespace+"="+namespace) - return filter -} - -func getServiceFilter(namespace string) filters.Args { - return getStackFilter(namespace) -} - -func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) filters.Args { - filter := opt.Value() - filter.Add("label", convert.LabelNamespace+"="+namespace) - return filter -} - -func getAllStacksFilter() filters.Args { - filter := filters.NewArgs() - filter.Add("label", convert.LabelNamespace) - return filter -} - -func getServices( - ctx context.Context, - apiclient client.APIClient, - namespace string, -) ([]swarm.Service, error) { - return apiclient.ServiceList( - ctx, - types.ServiceListOptions{Filters: getServiceFilter(namespace)}) -} - -func getStackNetworks( - ctx context.Context, - apiclient client.APIClient, - namespace string, -) ([]types.NetworkResource, error) { - return apiclient.NetworkList( - ctx, - types.NetworkListOptions{Filters: getStackFilter(namespace)}) -} - -func getStackSecrets( - ctx context.Context, - apiclient client.APIClient, - namespace string, -) ([]swarm.Secret, error) { - return apiclient.SecretList( - ctx, - types.SecretListOptions{Filters: getStackFilter(namespace)}) -} diff --git a/cli/command/stack/deploy.go b/cli/command/stack/deploy.go deleted file mode 100644 index 6789171702..0000000000 --- a/cli/command/stack/deploy.go +++ /dev/null @@ -1,97 +0,0 @@ -package stack - -import ( - "fmt" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/compose/convert" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -const ( - defaultNetworkDriver = "overlay" -) - -type deployOptions struct { - bundlefile string - composefile string - namespace string - sendRegistryAuth bool - prune bool -} - -func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts deployOptions - - cmd := &cobra.Command{ - Use: "deploy [OPTIONS] STACK", - Aliases: []string{"up"}, - Short: "Deploy a new stack or update an existing stack", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.namespace = args[0] - return runDeploy(dockerCli, opts) - }, - } - - flags := cmd.Flags() - addBundlefileFlag(&opts.bundlefile, flags) - addComposefileFlag(&opts.composefile, flags) - addRegistryAuthFlag(&opts.sendRegistryAuth, flags) - flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced") - flags.SetAnnotation("prune", "version", []string{"1.27"}) - return cmd -} - -func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { - ctx := context.Background() - - switch { - case opts.bundlefile == "" && opts.composefile == "": - return errors.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).") - case opts.bundlefile != "" && opts.composefile != "": - return errors.Errorf("You cannot specify both a bundle file and a Compose file.") - case opts.bundlefile != "": - return deployBundle(ctx, dockerCli, opts) - default: - return deployCompose(ctx, dockerCli, opts) - } -} - -// checkDaemonIsSwarmManager does an Info API call to verify that the daemon is -// a swarm manager. This is necessary because we must create networks before we -// create services, but the API call for creating a network does not return a -// proper status code when it can't create a network in the "global" scope. -func checkDaemonIsSwarmManager(ctx context.Context, dockerCli *command.DockerCli) error { - info, err := dockerCli.Client().Info(ctx) - if err != nil { - return err - } - if !info.Swarm.ControlAvailable { - return errors.New("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.") - } - return nil -} - -// pruneServices removes services that are no longer referenced in the source -func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert.Namespace, services map[string]struct{}) bool { - client := dockerCli.Client() - - oldServices, err := getServices(ctx, client, namespace.Name()) - if err != nil { - fmt.Fprintf(dockerCli.Err(), "Failed to list services: %s", err) - return true - } - - pruneServices := []swarm.Service{} - for _, service := range oldServices { - if _, exists := services[namespace.Descope(service.Spec.Name)]; !exists { - pruneServices = append(pruneServices, service) - } - } - return removeServices(ctx, dockerCli, pruneServices) -} diff --git a/cli/command/stack/deploy_bundlefile.go b/cli/command/stack/deploy_bundlefile.go deleted file mode 100644 index 0f8f8d040b..0000000000 --- a/cli/command/stack/deploy_bundlefile.go +++ /dev/null @@ -1,91 +0,0 @@ -package stack - -import ( - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/compose/convert" -) - -func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error { - bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) - if err != nil { - return err - } - - if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil { - return err - } - - namespace := convert.NewNamespace(opts.namespace) - - if opts.prune { - services := map[string]struct{}{} - for service := range bundle.Services { - services[service] = struct{}{} - } - pruneServices(ctx, dockerCli, namespace, services) - } - - networks := make(map[string]types.NetworkCreate) - for _, service := range bundle.Services { - for _, networkName := range service.Networks { - networks[networkName] = types.NetworkCreate{ - Labels: convert.AddStackLabel(namespace, nil), - } - } - } - - services := make(map[string]swarm.ServiceSpec) - for internalName, service := range bundle.Services { - name := namespace.Scope(internalName) - - var ports []swarm.PortConfig - for _, portSpec := range service.Ports { - ports = append(ports, swarm.PortConfig{ - Protocol: swarm.PortConfigProtocol(portSpec.Protocol), - TargetPort: portSpec.Port, - }) - } - - nets := []swarm.NetworkAttachmentConfig{} - for _, networkName := range service.Networks { - nets = append(nets, swarm.NetworkAttachmentConfig{ - Target: namespace.Scope(networkName), - Aliases: []string{internalName}, - }) - } - - serviceSpec := swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: name, - Labels: convert.AddStackLabel(namespace, service.Labels), - }, - TaskTemplate: swarm.TaskSpec{ - ContainerSpec: swarm.ContainerSpec{ - Image: service.Image, - Command: service.Command, - Args: service.Args, - Env: service.Env, - // Service Labels will not be copied to Containers - // automatically during the deployment so we apply - // it here. - Labels: convert.AddStackLabel(namespace, nil), - }, - }, - EndpointSpec: &swarm.EndpointSpec{ - Ports: ports, - }, - Networks: nets, - } - - services[internalName] = serviceSpec - } - - if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { - return err - } - return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) -} diff --git a/cli/command/stack/deploy_composefile.go b/cli/command/stack/deploy_composefile.go deleted file mode 100644 index 700a65dce4..0000000000 --- a/cli/command/stack/deploy_composefile.go +++ /dev/null @@ -1,316 +0,0 @@ -package stack - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "sort" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/compose/convert" - "github.com/docker/docker/cli/compose/loader" - composetypes "github.com/docker/docker/cli/compose/types" - apiclient "github.com/docker/docker/client" - dockerclient "github.com/docker/docker/client" - "github.com/pkg/errors" - "golang.org/x/net/context" -) - -func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error { - configDetails, err := getConfigDetails(opts.composefile) - if err != nil { - return err - } - - config, err := loader.Load(configDetails) - if err != nil { - if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok { - return errors.Errorf("Compose file contains unsupported options:\n\n%s\n", - propertyWarnings(fpe.Properties)) - } - - return err - } - - unsupportedProperties := loader.GetUnsupportedProperties(configDetails) - if len(unsupportedProperties) > 0 { - fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n", - strings.Join(unsupportedProperties, ", ")) - } - - deprecatedProperties := loader.GetDeprecatedProperties(configDetails) - if len(deprecatedProperties) > 0 { - fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n", - propertyWarnings(deprecatedProperties)) - } - - if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil { - return err - } - - namespace := convert.NewNamespace(opts.namespace) - - if opts.prune { - services := map[string]struct{}{} - for _, service := range config.Services { - services[service.Name] = struct{}{} - } - pruneServices(ctx, dockerCli, namespace, services) - } - - serviceNetworks := getServicesDeclaredNetworks(config.Services) - networks, externalNetworks := convert.Networks(namespace, config.Networks, serviceNetworks) - if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil { - return err - } - if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { - return err - } - - secrets, err := convert.Secrets(namespace, config.Secrets) - if err != nil { - return err - } - if err := createSecrets(ctx, dockerCli, namespace, secrets); err != nil { - return err - } - - services, err := convert.Services(namespace, config, dockerCli.Client()) - if err != nil { - return err - } - return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) -} - -func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} { - serviceNetworks := map[string]struct{}{} - for _, serviceConfig := range serviceConfigs { - if len(serviceConfig.Networks) == 0 { - serviceNetworks["default"] = struct{}{} - continue - } - for network := range serviceConfig.Networks { - serviceNetworks[network] = struct{}{} - } - } - return serviceNetworks -} - -func propertyWarnings(properties map[string]string) string { - var msgs []string - for name, description := range properties { - msgs = append(msgs, fmt.Sprintf("%s: %s", name, description)) - } - sort.Strings(msgs) - return strings.Join(msgs, "\n\n") -} - -func getConfigDetails(composefile string) (composetypes.ConfigDetails, error) { - var details composetypes.ConfigDetails - - absPath, err := filepath.Abs(composefile) - if err != nil { - return details, err - } - details.WorkingDir = filepath.Dir(absPath) - - configFile, err := getConfigFile(composefile) - if err != nil { - return details, err - } - // TODO: support multiple files - details.ConfigFiles = []composetypes.ConfigFile{*configFile} - details.Environment, err = buildEnvironment(os.Environ()) - if err != nil { - return details, err - } - return details, nil -} - -func buildEnvironment(env []string) (map[string]string, error) { - result := make(map[string]string, len(env)) - for _, s := range env { - // if value is empty, s is like "K=", not "K". - if !strings.Contains(s, "=") { - return result, errors.Errorf("unexpected environment %q", s) - } - kv := strings.SplitN(s, "=", 2) - result[kv[0]] = kv[1] - } - return result, nil -} - -func getConfigFile(filename string) (*composetypes.ConfigFile, error) { - bytes, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - config, err := loader.ParseYAML(bytes) - if err != nil { - return nil, err - } - return &composetypes.ConfigFile{ - Filename: filename, - Config: config, - }, nil -} - -func validateExternalNetworks( - ctx context.Context, - dockerCli *command.DockerCli, - externalNetworks []string) error { - client := dockerCli.Client() - - for _, networkName := range externalNetworks { - network, err := client.NetworkInspect(ctx, networkName, false) - if err != nil { - if dockerclient.IsErrNetworkNotFound(err) { - return errors.Errorf("network %q is declared as external, but could not be found. You need to create the network before the stack is deployed (with overlay driver)", networkName) - } - return err - } - if network.Scope != "swarm" { - return errors.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of %q", networkName, network.Scope, "swarm") - } - } - - return nil -} - -func createSecrets( - ctx context.Context, - dockerCli *command.DockerCli, - namespace convert.Namespace, - secrets []swarm.SecretSpec, -) error { - client := dockerCli.Client() - - for _, secretSpec := range secrets { - secret, _, err := client.SecretInspectWithRaw(ctx, secretSpec.Name) - if err == nil { - // secret already exists, then we update that - if err := client.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil { - return err - } - } else if apiclient.IsErrSecretNotFound(err) { - // secret does not exist, then we create a new one. - if _, err := client.SecretCreate(ctx, secretSpec); err != nil { - return err - } - } else { - return err - } - } - return nil -} - -func createNetworks( - ctx context.Context, - dockerCli *command.DockerCli, - namespace convert.Namespace, - networks map[string]types.NetworkCreate, -) error { - client := dockerCli.Client() - - existingNetworks, err := getStackNetworks(ctx, client, namespace.Name()) - if err != nil { - return err - } - - existingNetworkMap := make(map[string]types.NetworkResource) - for _, network := range existingNetworks { - existingNetworkMap[network.Name] = network - } - - for internalName, createOpts := range networks { - name := namespace.Scope(internalName) - if _, exists := existingNetworkMap[name]; exists { - continue - } - - if createOpts.Driver == "" { - createOpts.Driver = defaultNetworkDriver - } - - fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name) - if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil { - return err - } - } - - return nil -} - -func deployServices( - ctx context.Context, - dockerCli *command.DockerCli, - services map[string]swarm.ServiceSpec, - namespace convert.Namespace, - sendAuth bool, -) error { - apiClient := dockerCli.Client() - out := dockerCli.Out() - - existingServices, err := getServices(ctx, apiClient, namespace.Name()) - if err != nil { - return err - } - - existingServiceMap := make(map[string]swarm.Service) - for _, service := range existingServices { - existingServiceMap[service.Spec.Name] = service - } - - for internalName, serviceSpec := range services { - name := namespace.Scope(internalName) - - encodedAuth := "" - if sendAuth { - // Retrieve encoded auth token from the image reference - image := serviceSpec.TaskTemplate.ContainerSpec.Image - encodedAuth, err = command.RetrieveAuthTokenFromImage(ctx, dockerCli, image) - if err != nil { - return err - } - } - - if service, exists := existingServiceMap[name]; exists { - fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID) - - updateOpts := types.ServiceUpdateOptions{} - if sendAuth { - updateOpts.EncodedRegistryAuth = encodedAuth - } - response, err := apiClient.ServiceUpdate( - ctx, - service.ID, - service.Version, - serviceSpec, - updateOpts, - ) - if err != nil { - return err - } - - for _, warning := range response.Warnings { - fmt.Fprintln(dockerCli.Err(), warning) - } - } else { - fmt.Fprintf(out, "Creating service %s\n", name) - - createOpts := types.ServiceCreateOptions{} - if sendAuth { - createOpts.EncodedRegistryAuth = encodedAuth - } - if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil { - return err - } - } - } - - return nil -} diff --git a/cli/command/stack/deploy_composefile_test.go b/cli/command/stack/deploy_composefile_test.go deleted file mode 100644 index d5ef5463ff..0000000000 --- a/cli/command/stack/deploy_composefile_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package stack - -import ( - "os" - "path/filepath" - "testing" - - "github.com/docker/docker/pkg/testutil/tempfile" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetConfigDetails(t *testing.T) { - content := ` -version: "3.0" -services: - foo: - image: alpine:3.5 -` - file := tempfile.NewTempFile(t, "test-get-config-details", content) - defer file.Remove() - - details, err := getConfigDetails(file.Name()) - require.NoError(t, err) - assert.Equal(t, filepath.Dir(file.Name()), details.WorkingDir) - assert.Len(t, details.ConfigFiles, 1) - assert.Len(t, details.Environment, len(os.Environ())) -} diff --git a/cli/command/stack/deploy_test.go b/cli/command/stack/deploy_test.go deleted file mode 100644 index 817c06dd04..0000000000 --- a/cli/command/stack/deploy_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package stack - -import ( - "bytes" - "testing" - - "github.com/docker/docker/cli/compose/convert" - "github.com/docker/docker/cli/internal/test" - "github.com/stretchr/testify/assert" - "golang.org/x/net/context" -) - -func TestPruneServices(t *testing.T) { - ctx := context.Background() - namespace := convert.NewNamespace("foo") - services := map[string]struct{}{ - "new": {}, - "keep": {}, - } - client := &fakeClient{services: []string{objectName("foo", "keep"), objectName("foo", "remove")}} - dockerCli := test.NewFakeCli(client, &bytes.Buffer{}) - dockerCli.SetErr(&bytes.Buffer{}) - - pruneServices(ctx, dockerCli, namespace, services) - - assert.Equal(t, buildObjectIDs([]string{objectName("foo", "remove")}), client.removedServices) -} diff --git a/cli/command/stack/list.go b/cli/command/stack/list.go deleted file mode 100644 index 61f1e6b439..0000000000 --- a/cli/command/stack/list.go +++ /dev/null @@ -1,95 +0,0 @@ -package stack - -import ( - "sort" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/cli/compose/convert" - "github.com/docker/docker/client" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type listOptions struct { - format string -} - -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := listOptions{} - - cmd := &cobra.Command{ - Use: "ls", - Aliases: []string{"list"}, - Short: "List stacks", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVar(&opts.format, "format", "", "Pretty-print stacks using a Go template") - return cmd -} - -func runList(dockerCli *command.DockerCli, opts listOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - stacks, err := getStacks(ctx, client) - if err != nil { - return err - } - format := opts.format - if len(format) == 0 { - format = formatter.TableFormatKey - } - stackCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewStackFormat(format), - } - sort.Sort(byName(stacks)) - return formatter.StackWrite(stackCtx, stacks) -} - -type byName []*formatter.Stack - -func (n byName) Len() int { return len(n) } -func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] } -func (n byName) Less(i, j int) bool { return n[i].Name < n[j].Name } - -func getStacks(ctx context.Context, apiclient client.APIClient) ([]*formatter.Stack, error) { - services, err := apiclient.ServiceList( - ctx, - types.ServiceListOptions{Filters: getAllStacksFilter()}) - if err != nil { - return nil, err - } - m := make(map[string]*formatter.Stack, 0) - for _, service := range services { - labels := service.Spec.Labels - name, ok := labels[convert.LabelNamespace] - if !ok { - return nil, errors.Errorf("cannot get label %s for service %s", - convert.LabelNamespace, service.ID) - } - ztack, ok := m[name] - if !ok { - m[name] = &formatter.Stack{ - Name: name, - Services: 1, - } - } else { - ztack.Services++ - } - } - var stacks []*formatter.Stack - for _, stack := range m { - stacks = append(stacks, stack) - } - return stacks, nil -} diff --git a/cli/command/stack/opts.go b/cli/command/stack/opts.go deleted file mode 100644 index 0d7214e962..0000000000 --- a/cli/command/stack/opts.go +++ /dev/null @@ -1,51 +0,0 @@ -package stack - -import ( - "fmt" - "io" - "os" - - "github.com/docker/docker/cli/command/bundlefile" - "github.com/pkg/errors" - "github.com/spf13/pflag" -) - -func addComposefileFlag(opt *string, flags *pflag.FlagSet) { - flags.StringVarP(opt, "compose-file", "c", "", "Path to a Compose file") - flags.SetAnnotation("compose-file", "version", []string{"1.25"}) -} - -func addBundlefileFlag(opt *string, flags *pflag.FlagSet) { - flags.StringVar(opt, "bundle-file", "", "Path to a Distributed Application Bundle file") - flags.SetAnnotation("bundle-file", "experimental", nil) -} - -func addRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) { - flags.BoolVar(opt, "with-registry-auth", false, "Send registry authentication details to Swarm agents") -} - -func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) { - defaultPath := fmt.Sprintf("%s.dab", namespace) - - if path == "" { - path = defaultPath - } - if _, err := os.Stat(path); err != nil { - return nil, errors.Errorf( - "Bundle %s not found. Specify the path with --file", - path) - } - - fmt.Fprintf(stderr, "Loading bundle from %s\n", path) - reader, err := os.Open(path) - if err != nil { - return nil, err - } - defer reader.Close() - - bundle, err := bundlefile.LoadFile(reader) - if err != nil { - return nil, errors.Errorf("Error reading %s: %v\n", path, err) - } - return bundle, err -} diff --git a/cli/command/stack/ps.go b/cli/command/stack/ps.go deleted file mode 100644 index bac5307bd1..0000000000 --- a/cli/command/stack/ps.go +++ /dev/null @@ -1,76 +0,0 @@ -package stack - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/cli/command/idresolver" - "github.com/docker/docker/cli/command/task" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" -) - -type psOptions struct { - filter opts.FilterOpt - noTrunc bool - namespace string - noResolve bool - quiet bool - format string -} - -func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := psOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ps [OPTIONS] STACK", - Short: "List the tasks in the stack", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.namespace = args[0] - return runPS(dockerCli, opts) - }, - } - flags := cmd.Flags() - flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") - flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs") - flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template") - - return cmd -} - -func runPS(dockerCli *command.DockerCli, opts psOptions) error { - namespace := opts.namespace - client := dockerCli.Client() - ctx := context.Background() - - filter := getStackFilterFromOpt(opts.namespace, opts.filter) - - tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter}) - if err != nil { - return err - } - - if len(tasks) == 0 { - fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace) - return nil - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().TasksFormat - } else { - format = formatter.TableFormatKey - } - } - - return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format) -} diff --git a/cli/command/stack/remove.go b/cli/command/stack/remove.go deleted file mode 100644 index 7df4e4c0ed..0000000000 --- a/cli/command/stack/remove.go +++ /dev/null @@ -1,121 +0,0 @@ -package stack - -import ( - "fmt" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type removeOptions struct { - namespaces []string -} - -func newRemoveCommand(dockerCli command.Cli) *cobra.Command { - var opts removeOptions - - cmd := &cobra.Command{ - Use: "rm STACK [STACK...]", - Aliases: []string{"remove", "down"}, - Short: "Remove one or more stacks", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.namespaces = args - return runRemove(dockerCli, opts) - }, - } - return cmd -} - -func runRemove(dockerCli command.Cli, opts removeOptions) error { - namespaces := opts.namespaces - client := dockerCli.Client() - ctx := context.Background() - - var errs []string - for _, namespace := range namespaces { - services, err := getServices(ctx, client, namespace) - if err != nil { - return err - } - - networks, err := getStackNetworks(ctx, client, namespace) - if err != nil { - return err - } - - secrets, err := getStackSecrets(ctx, client, namespace) - if err != nil { - return err - } - - if len(services)+len(networks)+len(secrets) == 0 { - fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace) - continue - } - - hasError := removeServices(ctx, dockerCli, services) - hasError = removeSecrets(ctx, dockerCli, secrets) || hasError - hasError = removeNetworks(ctx, dockerCli, networks) || hasError - - if hasError { - errs = append(errs, fmt.Sprintf("Failed to remove some resources from stack: %s", namespace)) - } - } - - if len(errs) > 0 { - return errors.Errorf(strings.Join(errs, "\n")) - } - return nil -} - -func removeServices( - ctx context.Context, - dockerCli command.Cli, - services []swarm.Service, -) bool { - var err error - for _, service := range services { - fmt.Fprintf(dockerCli.Err(), "Removing service %s\n", service.Spec.Name) - if err = dockerCli.Client().ServiceRemove(ctx, service.ID); err != nil { - fmt.Fprintf(dockerCli.Err(), "Failed to remove service %s: %s", service.ID, err) - } - } - return err != nil -} - -func removeNetworks( - ctx context.Context, - dockerCli command.Cli, - networks []types.NetworkResource, -) bool { - var err error - for _, network := range networks { - fmt.Fprintf(dockerCli.Err(), "Removing network %s\n", network.Name) - if err = dockerCli.Client().NetworkRemove(ctx, network.ID); err != nil { - fmt.Fprintf(dockerCli.Err(), "Failed to remove network %s: %s", network.ID, err) - } - } - return err != nil -} - -func removeSecrets( - ctx context.Context, - dockerCli command.Cli, - secrets []swarm.Secret, -) bool { - var err error - for _, secret := range secrets { - fmt.Fprintf(dockerCli.Err(), "Removing secret %s\n", secret.Spec.Name) - if err = dockerCli.Client().SecretRemove(ctx, secret.ID); err != nil { - fmt.Fprintf(dockerCli.Err(), "Failed to remove secret %s: %s", secret.ID, err) - } - } - return err != nil -} diff --git a/cli/command/stack/remove_test.go b/cli/command/stack/remove_test.go deleted file mode 100644 index 17a334db1e..0000000000 --- a/cli/command/stack/remove_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package stack - -import ( - "bytes" - "errors" - "strings" - "testing" - - "github.com/docker/docker/cli/internal/test" - "github.com/stretchr/testify/assert" -) - -func TestRemoveStack(t *testing.T) { - allServices := []string{ - objectName("foo", "service1"), - objectName("foo", "service2"), - objectName("bar", "service1"), - objectName("bar", "service2"), - } - allServiceIDs := buildObjectIDs(allServices) - - allNetworks := []string{ - objectName("foo", "network1"), - objectName("bar", "network1"), - } - allNetworkIDs := buildObjectIDs(allNetworks) - - allSecrets := []string{ - objectName("foo", "secret1"), - objectName("foo", "secret2"), - objectName("bar", "secret1"), - } - allSecretIDs := buildObjectIDs(allSecrets) - - cli := &fakeClient{ - services: allServices, - networks: allNetworks, - secrets: allSecrets, - } - cmd := newRemoveCommand(test.NewFakeCli(cli, &bytes.Buffer{})) - cmd.SetArgs([]string{"foo", "bar"}) - - assert.NoError(t, cmd.Execute()) - assert.Equal(t, allServiceIDs, cli.removedServices) - assert.Equal(t, allNetworkIDs, cli.removedNetworks) - assert.Equal(t, allSecretIDs, cli.removedSecrets) -} - -func TestSkipEmptyStack(t *testing.T) { - buf := new(bytes.Buffer) - allServices := []string{objectName("bar", "service1"), objectName("bar", "service2")} - allServiceIDs := buildObjectIDs(allServices) - - allNetworks := []string{objectName("bar", "network1")} - allNetworkIDs := buildObjectIDs(allNetworks) - - allSecrets := []string{objectName("bar", "secret1")} - allSecretIDs := buildObjectIDs(allSecrets) - - cli := &fakeClient{ - services: allServices, - networks: allNetworks, - secrets: allSecrets, - } - cmd := newRemoveCommand(test.NewFakeCli(cli, buf)) - cmd.SetArgs([]string{"foo", "bar"}) - - assert.NoError(t, cmd.Execute()) - assert.Contains(t, buf.String(), "Nothing found in stack: foo") - assert.Equal(t, allServiceIDs, cli.removedServices) - assert.Equal(t, allNetworkIDs, cli.removedNetworks) - assert.Equal(t, allSecretIDs, cli.removedSecrets) -} - -func TestContinueAfterError(t *testing.T) { - allServices := []string{objectName("foo", "service1"), objectName("bar", "service1")} - allServiceIDs := buildObjectIDs(allServices) - - allNetworks := []string{objectName("foo", "network1"), objectName("bar", "network1")} - allNetworkIDs := buildObjectIDs(allNetworks) - - allSecrets := []string{objectName("foo", "secret1"), objectName("bar", "secret1")} - allSecretIDs := buildObjectIDs(allSecrets) - - removedServices := []string{} - cli := &fakeClient{ - services: allServices, - networks: allNetworks, - secrets: allSecrets, - - serviceRemoveFunc: func(serviceID string) error { - removedServices = append(removedServices, serviceID) - - if strings.Contains(serviceID, "foo") { - return errors.New("") - } - return nil - }, - } - cmd := newRemoveCommand(test.NewFakeCli(cli, &bytes.Buffer{})) - cmd.SetArgs([]string{"foo", "bar"}) - - assert.EqualError(t, cmd.Execute(), "Failed to remove some resources from stack: foo") - assert.Equal(t, allServiceIDs, removedServices) - assert.Equal(t, allNetworkIDs, cli.removedNetworks) - assert.Equal(t, allSecretIDs, cli.removedSecrets) -} diff --git a/cli/command/stack/services.go b/cli/command/stack/services.go deleted file mode 100644 index 78ddd399ce..0000000000 --- a/cli/command/stack/services.go +++ /dev/null @@ -1,97 +0,0 @@ -package stack - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/cli/command/service" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" -) - -type servicesOptions struct { - quiet bool - format string - filter opts.FilterOpt - namespace string -} - -func newServicesCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := servicesOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "services [OPTIONS] STACK", - Short: "List the services in the stack", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.namespace = args[0] - return runServices(dockerCli, opts) - }, - } - flags := cmd.Flags() - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") - flags.StringVar(&opts.format, "format", "", "Pretty-print services using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - - return cmd -} - -func runServices(dockerCli *command.DockerCli, opts servicesOptions) error { - ctx := context.Background() - client := dockerCli.Client() - - filter := getStackFilterFromOpt(opts.namespace, opts.filter) - services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: filter}) - if err != nil { - return err - } - - out := dockerCli.Out() - - // if no services in this stack, print message and exit 0 - if len(services) == 0 { - fmt.Fprintf(out, "Nothing found in stack: %s\n", opts.namespace) - return nil - } - - info := map[string]formatter.ServiceListInfo{} - if !opts.quiet { - taskFilter := filters.NewArgs() - for _, service := range services { - taskFilter.Add("service", service.ID) - } - - tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter}) - if err != nil { - return err - } - - nodes, err := client.NodeList(ctx, types.NodeListOptions{}) - if err != nil { - return err - } - - info = service.GetServicesStatus(services, nodes, tasks) - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().ServicesFormat - } else { - format = formatter.TableFormatKey - } - } - - servicesCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewServiceListFormat(format, opts.quiet), - } - return formatter.ServiceListWrite(servicesCtx, services, info) -} diff --git a/cli/command/stream.go b/cli/command/stream.go deleted file mode 100644 index 71a43fa2e9..0000000000 --- a/cli/command/stream.go +++ /dev/null @@ -1,34 +0,0 @@ -package command - -import ( - "github.com/docker/docker/pkg/term" -) - -// CommonStream is an input stream used by the DockerCli to read user input -type CommonStream struct { - fd uintptr - isTerminal bool - state *term.State -} - -// FD returns the file descriptor number for this stream -func (s *CommonStream) FD() uintptr { - return s.fd -} - -// IsTerminal returns true if this stream is connected to a terminal -func (s *CommonStream) IsTerminal() bool { - return s.isTerminal -} - -// RestoreTerminal restores normal mode to the terminal -func (s *CommonStream) RestoreTerminal() { - if s.state != nil { - term.RestoreTerminal(s.fd, s.state) - } -} - -// SetIsTerminal sets the boolean used for isTerminal -func (s *CommonStream) SetIsTerminal(isTerminal bool) { - s.isTerminal = isTerminal -} diff --git a/cli/command/swarm/client_test.go b/cli/command/swarm/client_test.go deleted file mode 100644 index 1d42b9499c..0000000000 --- a/cli/command/swarm/client_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package swarm - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -type fakeClient struct { - client.Client - infoFunc func() (types.Info, error) - swarmInitFunc func() (string, error) - swarmInspectFunc func() (swarm.Swarm, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) - swarmJoinFunc func() error - swarmLeaveFunc func() error - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - swarmUnlockFunc func(req swarm.UnlockRequest) error -} - -func (cli *fakeClient) Info(ctx context.Context) (types.Info, error) { - if cli.infoFunc != nil { - return cli.infoFunc() - } - return types.Info{}, nil -} - -func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, ref string) (swarm.Node, []byte, error) { - if cli.nodeInspectFunc != nil { - return cli.nodeInspectFunc() - } - return swarm.Node{}, []byte{}, nil -} - -func (cli *fakeClient) SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error) { - if cli.swarmInitFunc != nil { - return cli.swarmInitFunc() - } - return "", nil -} - -func (cli *fakeClient) SwarmInspect(ctx context.Context) (swarm.Swarm, error) { - if cli.swarmInspectFunc != nil { - return cli.swarmInspectFunc() - } - return swarm.Swarm{}, nil -} - -func (cli *fakeClient) SwarmGetUnlockKey(ctx context.Context) (types.SwarmUnlockKeyResponse, error) { - if cli.swarmGetUnlockKeyFunc != nil { - return cli.swarmGetUnlockKeyFunc() - } - return types.SwarmUnlockKeyResponse{}, nil -} - -func (cli *fakeClient) SwarmJoin(ctx context.Context, req swarm.JoinRequest) error { - if cli.swarmJoinFunc != nil { - return cli.swarmJoinFunc() - } - return nil -} - -func (cli *fakeClient) SwarmLeave(ctx context.Context, force bool) error { - if cli.swarmLeaveFunc != nil { - return cli.swarmLeaveFunc() - } - return nil -} - -func (cli *fakeClient) SwarmUpdate(ctx context.Context, version swarm.Version, swarm swarm.Spec, flags swarm.UpdateFlags) error { - if cli.swarmUpdateFunc != nil { - return cli.swarmUpdateFunc(swarm, flags) - } - return nil -} - -func (cli *fakeClient) SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error { - if cli.swarmUnlockFunc != nil { - return cli.swarmUnlockFunc(req) - } - return nil -} diff --git a/cli/command/swarm/cmd.go b/cli/command/swarm/cmd.go deleted file mode 100644 index 659dbcdf7b..0000000000 --- a/cli/command/swarm/cmd.go +++ /dev/null @@ -1,29 +0,0 @@ -package swarm - -import ( - "github.com/spf13/cobra" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" -) - -// NewSwarmCommand returns a cobra command for `swarm` subcommands -func NewSwarmCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "swarm", - Short: "Manage Swarm", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - Tags: map[string]string{"version": "1.24"}, - } - cmd.AddCommand( - newInitCommand(dockerCli), - newJoinCommand(dockerCli), - newJoinTokenCommand(dockerCli), - newUnlockKeyCommand(dockerCli), - newUpdateCommand(dockerCli), - newLeaveCommand(dockerCli), - newUnlockCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/swarm/init.go b/cli/command/swarm/init.go deleted file mode 100644 index 5d42b0174c..0000000000 --- a/cli/command/swarm/init.go +++ /dev/null @@ -1,99 +0,0 @@ -package swarm - -import ( - "fmt" - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -type initOptions struct { - swarmOptions - listenAddr NodeAddrOption - // Not a NodeAddrOption because it has no default port. - advertiseAddr string - dataPathAddr string - forceNewCluster bool - availability string -} - -func newInitCommand(dockerCli command.Cli) *cobra.Command { - opts := initOptions{ - listenAddr: NewListenAddrOption(), - } - - cmd := &cobra.Command{ - Use: "init [OPTIONS]", - Short: "Initialize a swarm", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runInit(dockerCli, cmd.Flags(), opts) - }, - } - - flags := cmd.Flags() - flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: [:port])") - flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") - flags.StringVar(&opts.dataPathAddr, flagDataPathAddr, "", "Address or interface to use for data path traffic (format: )") - flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state") - flags.BoolVar(&opts.autolock, flagAutolock, false, "Enable manager autolocking (requiring an unlock key to start a stopped manager)") - flags.StringVar(&opts.availability, flagAvailability, "active", `Availability of the node ("active"|"pause"|"drain")`) - addSwarmFlags(flags, &opts.swarmOptions) - return cmd -} - -func runInit(dockerCli command.Cli, flags *pflag.FlagSet, opts initOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - req := swarm.InitRequest{ - ListenAddr: opts.listenAddr.String(), - AdvertiseAddr: opts.advertiseAddr, - DataPathAddr: opts.dataPathAddr, - ForceNewCluster: opts.forceNewCluster, - Spec: opts.swarmOptions.ToSpec(flags), - AutoLockManagers: opts.swarmOptions.autolock, - } - if flags.Changed(flagAvailability) { - availability := swarm.NodeAvailability(strings.ToLower(opts.availability)) - switch availability { - case swarm.NodeAvailabilityActive, swarm.NodeAvailabilityPause, swarm.NodeAvailabilityDrain: - req.Availability = availability - default: - return errors.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) - } - } - - nodeID, err := client.SwarmInit(ctx, req) - if err != nil { - if strings.Contains(err.Error(), "could not choose an IP address to advertise") || strings.Contains(err.Error(), "could not find the system's IP address") { - return errors.New(err.Error() + " - specify one with --advertise-addr") - } - return err - } - - fmt.Fprintf(dockerCli.Out(), "Swarm initialized: current node (%s) is now a manager.\n\n", nodeID) - - if err := printJoinCommand(ctx, dockerCli, nodeID, true, false); err != nil { - return err - } - - fmt.Fprint(dockerCli.Out(), "To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.\n\n") - - if req.AutoLockManagers { - unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) - if err != nil { - return errors.Wrap(err, "could not fetch unlock key") - } - printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey) - } - - return nil -} diff --git a/cli/command/swarm/init_test.go b/cli/command/swarm/init_test.go deleted file mode 100644 index 39cb73888c..0000000000 --- a/cli/command/swarm/init_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package swarm - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestSwarmInitErrorOnAPIFailure(t *testing.T) { - testCases := []struct { - name string - flags map[string]string - swarmInitFunc func() (string, error) - swarmInspectFunc func() (swarm.Swarm, error) - swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - expectedError string - }{ - { - name: "init-failed", - swarmInitFunc: func() (string, error) { - return "", errors.Errorf("error initializing the swarm") - }, - expectedError: "error initializing the swarm", - }, - { - name: "init-failed-with-ip-choice", - swarmInitFunc: func() (string, error) { - return "", errors.Errorf("could not choose an IP address to advertise") - }, - expectedError: "could not choose an IP address to advertise - specify one with --advertise-addr", - }, - { - name: "swarm-inspect-after-init-failed", - swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, errors.Errorf("error inspecting the swarm") - }, - expectedError: "error inspecting the swarm", - }, - { - name: "node-inspect-after-init-failed", - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") - }, - expectedError: "error inspecting the node", - }, - { - name: "swarm-get-unlock-key-after-init-failed", - flags: map[string]string{ - flagAutolock: "true", - }, - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{}, errors.Errorf("error getting swarm unlock key") - }, - expectedError: "could not fetch unlock key: error getting swarm unlock key", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newInitCommand( - test.NewFakeCli(&fakeClient{ - swarmInitFunc: tc.swarmInitFunc, - swarmInspectFunc: tc.swarmInspectFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, - nodeInspectFunc: tc.nodeInspectFunc, - }, buf)) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - assert.EqualError(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSwarmInit(t *testing.T) { - testCases := []struct { - name string - flags map[string]string - swarmInitFunc func() (string, error) - swarmInspectFunc func() (swarm.Swarm, error) - swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - }{ - { - name: "init", - swarmInitFunc: func() (string, error) { - return "nodeID", nil - }, - }, - { - name: "init-autolock", - flags: map[string]string{ - flagAutolock: "true", - }, - swarmInitFunc: func() (string, error) { - return "nodeID", nil - }, - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{ - UnlockKey: "unlock-key", - }, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newInitCommand( - test.NewFakeCli(&fakeClient{ - swarmInitFunc: tc.swarmInitFunc, - swarmInspectFunc: tc.swarmInspectFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, - nodeInspectFunc: tc.nodeInspectFunc, - }, buf)) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("init-%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} diff --git a/cli/command/swarm/join.go b/cli/command/swarm/join.go deleted file mode 100644 index 34730ab8c7..0000000000 --- a/cli/command/swarm/join.go +++ /dev/null @@ -1,88 +0,0 @@ -package swarm - -import ( - "fmt" - "strings" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -type joinOptions struct { - remote string - listenAddr NodeAddrOption - // Not a NodeAddrOption because it has no default port. - advertiseAddr string - dataPathAddr string - token string - availability string -} - -func newJoinCommand(dockerCli command.Cli) *cobra.Command { - opts := joinOptions{ - listenAddr: NewListenAddrOption(), - } - - cmd := &cobra.Command{ - Use: "join [OPTIONS] HOST:PORT", - Short: "Join a swarm as a node and/or manager", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.remote = args[0] - return runJoin(dockerCli, cmd.Flags(), opts) - }, - } - - flags := cmd.Flags() - flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: [:port])") - flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") - flags.StringVar(&opts.dataPathAddr, flagDataPathAddr, "", "Address or interface to use for data path traffic (format: )") - flags.StringVar(&opts.token, flagToken, "", "Token for entry into the swarm") - flags.StringVar(&opts.availability, flagAvailability, "active", `Availability of the node ("active"|"pause"|"drain")`) - return cmd -} - -func runJoin(dockerCli command.Cli, flags *pflag.FlagSet, opts joinOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - req := swarm.JoinRequest{ - JoinToken: opts.token, - ListenAddr: opts.listenAddr.String(), - AdvertiseAddr: opts.advertiseAddr, - DataPathAddr: opts.dataPathAddr, - RemoteAddrs: []string{opts.remote}, - } - if flags.Changed(flagAvailability) { - availability := swarm.NodeAvailability(strings.ToLower(opts.availability)) - switch availability { - case swarm.NodeAvailabilityActive, swarm.NodeAvailabilityPause, swarm.NodeAvailabilityDrain: - req.Availability = availability - default: - return errors.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) - } - } - - err := client.SwarmJoin(ctx, req) - if err != nil { - return err - } - - info, err := client.Info(ctx) - if err != nil { - return err - } - - if info.Swarm.ControlAvailable { - fmt.Fprintln(dockerCli.Out(), "This node joined a swarm as a manager.") - } else { - fmt.Fprintln(dockerCli.Out(), "This node joined a swarm as a worker.") - } - return nil -} diff --git a/cli/command/swarm/join_test.go b/cli/command/swarm/join_test.go deleted file mode 100644 index 6893f68e1d..0000000000 --- a/cli/command/swarm/join_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package swarm - -import ( - "bytes" - "io/ioutil" - "strings" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestSwarmJoinErrors(t *testing.T) { - testCases := []struct { - name string - args []string - swarmJoinFunc func() error - infoFunc func() (types.Info, error) - expectedError string - }{ - { - name: "not-enough-args", - expectedError: "requires exactly 1 argument", - }, - { - name: "too-many-args", - args: []string{"remote1", "remote2"}, - expectedError: "requires exactly 1 argument", - }, - { - name: "join-failed", - args: []string{"remote"}, - swarmJoinFunc: func() error { - return errors.Errorf("error joining the swarm") - }, - expectedError: "error joining the swarm", - }, - { - name: "join-failed-on-init", - args: []string{"remote"}, - infoFunc: func() (types.Info, error) { - return types.Info{}, errors.Errorf("error asking for node info") - }, - expectedError: "error asking for node info", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newJoinCommand( - test.NewFakeCli(&fakeClient{ - swarmJoinFunc: tc.swarmJoinFunc, - infoFunc: tc.infoFunc, - }, buf)) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSwarmJoin(t *testing.T) { - testCases := []struct { - name string - infoFunc func() (types.Info, error) - expected string - }{ - { - name: "join-as-manager", - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - ControlAvailable: true, - }, - }, nil - }, - expected: "This node joined a swarm as a manager.", - }, - { - name: "join-as-worker", - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - ControlAvailable: false, - }, - }, nil - }, - expected: "This node joined a swarm as a worker.", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newJoinCommand( - test.NewFakeCli(&fakeClient{ - infoFunc: tc.infoFunc, - }, buf)) - cmd.SetArgs([]string{"remote"}) - assert.NoError(t, cmd.Execute()) - assert.Equal(t, strings.TrimSpace(buf.String()), tc.expected) - } -} diff --git a/cli/command/swarm/join_token.go b/cli/command/swarm/join_token.go deleted file mode 100644 index 465f379c2d..0000000000 --- a/cli/command/swarm/join_token.go +++ /dev/null @@ -1,119 +0,0 @@ -package swarm - -import ( - "fmt" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type joinTokenOptions struct { - role string - rotate bool - quiet bool -} - -func newJoinTokenCommand(dockerCli command.Cli) *cobra.Command { - opts := joinTokenOptions{} - - cmd := &cobra.Command{ - Use: "join-token [OPTIONS] (worker|manager)", - Short: "Manage join tokens", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.role = args[0] - return runJoinToken(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVar(&opts.rotate, flagRotate, false, "Rotate join token") - flags.BoolVarP(&opts.quiet, flagQuiet, "q", false, "Only display token") - - return cmd -} - -func runJoinToken(dockerCli command.Cli, opts joinTokenOptions) error { - worker := opts.role == "worker" - manager := opts.role == "manager" - - if !worker && !manager { - return errors.New("unknown role " + opts.role) - } - - client := dockerCli.Client() - ctx := context.Background() - - if opts.rotate { - flags := swarm.UpdateFlags{ - RotateWorkerToken: worker, - RotateManagerToken: manager, - } - - sw, err := client.SwarmInspect(ctx) - if err != nil { - return err - } - - if err := client.SwarmUpdate(ctx, sw.Version, sw.Spec, flags); err != nil { - return err - } - - if !opts.quiet { - fmt.Fprintf(dockerCli.Out(), "Successfully rotated %s join token.\n\n", opts.role) - } - } - - // second SwarmInspect in this function, - // this is necessary since SwarmUpdate after first changes the join tokens - sw, err := client.SwarmInspect(ctx) - if err != nil { - return err - } - - if opts.quiet && worker { - fmt.Fprintln(dockerCli.Out(), sw.JoinTokens.Worker) - return nil - } - - if opts.quiet && manager { - fmt.Fprintln(dockerCli.Out(), sw.JoinTokens.Manager) - return nil - } - - info, err := client.Info(ctx) - if err != nil { - return err - } - - return printJoinCommand(ctx, dockerCli, info.Swarm.NodeID, worker, manager) -} - -func printJoinCommand(ctx context.Context, dockerCli command.Cli, nodeID string, worker bool, manager bool) error { - client := dockerCli.Client() - - node, _, err := client.NodeInspectWithRaw(ctx, nodeID) - if err != nil { - return err - } - - sw, err := client.SwarmInspect(ctx) - if err != nil { - return err - } - - if node.ManagerStatus != nil { - if worker { - fmt.Fprintf(dockerCli.Out(), "To add a worker to this swarm, run the following command:\n\n docker swarm join --token %s %s\n\n", sw.JoinTokens.Worker, node.ManagerStatus.Addr) - } - if manager { - fmt.Fprintf(dockerCli.Out(), "To add a manager to this swarm, run the following command:\n\n docker swarm join --token %s %s\n\n", sw.JoinTokens.Manager, node.ManagerStatus.Addr) - } - } - - return nil -} diff --git a/cli/command/swarm/join_token_test.go b/cli/command/swarm/join_token_test.go deleted file mode 100644 index 9289189009..0000000000 --- a/cli/command/swarm/join_token_test.go +++ /dev/null @@ -1,217 +0,0 @@ -package swarm - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestSwarmJoinTokenErrors(t *testing.T) { - testCases := []struct { - name string - args []string - flags map[string]string - infoFunc func() (types.Info, error) - swarmInspectFunc func() (swarm.Swarm, error) - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - nodeInspectFunc func() (swarm.Node, []byte, error) - expectedError string - }{ - { - name: "not-enough-args", - expectedError: "requires exactly 1 argument", - }, - { - name: "too-many-args", - args: []string{"worker", "manager"}, - expectedError: "requires exactly 1 argument", - }, - { - name: "invalid-args", - args: []string{"foo"}, - expectedError: "unknown role foo", - }, - { - name: "swarm-inspect-failed", - args: []string{"worker"}, - swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, errors.Errorf("error inspecting the swarm") - }, - expectedError: "error inspecting the swarm", - }, - { - name: "swarm-inspect-rotate-failed", - args: []string{"worker"}, - flags: map[string]string{ - flagRotate: "true", - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, errors.Errorf("error inspecting the swarm") - }, - expectedError: "error inspecting the swarm", - }, - { - name: "swarm-update-failed", - args: []string{"worker"}, - flags: map[string]string{ - flagRotate: "true", - }, - swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - return errors.Errorf("error updating the swarm") - }, - expectedError: "error updating the swarm", - }, - { - name: "node-inspect-failed", - args: []string{"worker"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.Errorf("error inspecting node") - }, - expectedError: "error inspecting node", - }, - { - name: "info-failed", - args: []string{"worker"}, - infoFunc: func() (types.Info, error) { - return types.Info{}, errors.Errorf("error asking for node info") - }, - expectedError: "error asking for node info", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newJoinTokenCommand( - test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - swarmUpdateFunc: tc.swarmUpdateFunc, - infoFunc: tc.infoFunc, - nodeInspectFunc: tc.nodeInspectFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSwarmJoinToken(t *testing.T) { - testCases := []struct { - name string - args []string - flags map[string]string - infoFunc func() (types.Info, error) - swarmInspectFunc func() (swarm.Swarm, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - }{ - { - name: "worker", - args: []string{"worker"}, - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - NodeID: "nodeID", - }, - }, nil - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager()), []byte{}, nil - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(), nil - }, - }, - { - name: "manager", - args: []string{"manager"}, - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - NodeID: "nodeID", - }, - }, nil - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager()), []byte{}, nil - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(), nil - }, - }, - { - name: "manager-rotate", - args: []string{"manager"}, - flags: map[string]string{ - flagRotate: "true", - }, - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - NodeID: "nodeID", - }, - }, nil - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager()), []byte{}, nil - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(), nil - }, - }, - { - name: "worker-quiet", - args: []string{"worker"}, - flags: map[string]string{ - flagQuiet: "true", - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager()), []byte{}, nil - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(), nil - }, - }, - { - name: "manager-quiet", - args: []string{"manager"}, - flags: map[string]string{ - flagQuiet: "true", - }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *Node(Manager()), []byte{}, nil - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(), nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newJoinTokenCommand( - test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - infoFunc: tc.infoFunc, - nodeInspectFunc: tc.nodeInspectFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("jointoken-%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} diff --git a/cli/command/swarm/leave.go b/cli/command/swarm/leave.go deleted file mode 100644 index 128ed46d8a..0000000000 --- a/cli/command/swarm/leave.go +++ /dev/null @@ -1,44 +0,0 @@ -package swarm - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -type leaveOptions struct { - force bool -} - -func newLeaveCommand(dockerCli command.Cli) *cobra.Command { - opts := leaveOptions{} - - cmd := &cobra.Command{ - Use: "leave [OPTIONS]", - Short: "Leave the swarm", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runLeave(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Force this node to leave the swarm, ignoring warnings") - return cmd -} - -func runLeave(dockerCli command.Cli, opts leaveOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - if err := client.SwarmLeave(ctx, opts.force); err != nil { - return err - } - - fmt.Fprintln(dockerCli.Out(), "Node left the swarm.") - return nil -} diff --git a/cli/command/swarm/leave_test.go b/cli/command/swarm/leave_test.go deleted file mode 100644 index 030f18039a..0000000000 --- a/cli/command/swarm/leave_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package swarm - -import ( - "bytes" - "io/ioutil" - "strings" - "testing" - - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestSwarmLeaveErrors(t *testing.T) { - testCases := []struct { - name string - args []string - swarmLeaveFunc func() error - expectedError string - }{ - { - name: "too-many-args", - args: []string{"foo"}, - expectedError: "accepts no argument(s)", - }, - { - name: "leave-failed", - swarmLeaveFunc: func() error { - return errors.Errorf("error leaving the swarm") - }, - expectedError: "error leaving the swarm", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newLeaveCommand( - test.NewFakeCli(&fakeClient{ - swarmLeaveFunc: tc.swarmLeaveFunc, - }, buf)) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSwarmLeave(t *testing.T) { - buf := new(bytes.Buffer) - cmd := newLeaveCommand( - test.NewFakeCli(&fakeClient{}, buf)) - assert.NoError(t, cmd.Execute()) - assert.Equal(t, "Node left the swarm.", strings.TrimSpace(buf.String())) -} diff --git a/cli/command/swarm/opts.go b/cli/command/swarm/opts.go deleted file mode 100644 index d53498586a..0000000000 --- a/cli/command/swarm/opts.go +++ /dev/null @@ -1,224 +0,0 @@ -package swarm - -import ( - "encoding/csv" - "encoding/pem" - "fmt" - "io/ioutil" - "strings" - "time" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/opts" - "github.com/pkg/errors" - "github.com/spf13/pflag" -) - -const ( - defaultListenAddr = "0.0.0.0:2377" - - flagCertExpiry = "cert-expiry" - flagDispatcherHeartbeat = "dispatcher-heartbeat" - flagListenAddr = "listen-addr" - flagAdvertiseAddr = "advertise-addr" - flagDataPathAddr = "data-path-addr" - flagQuiet = "quiet" - flagRotate = "rotate" - flagToken = "token" - flagTaskHistoryLimit = "task-history-limit" - flagExternalCA = "external-ca" - flagMaxSnapshots = "max-snapshots" - flagSnapshotInterval = "snapshot-interval" - flagLockKey = "lock-key" - flagAutolock = "autolock" - flagAvailability = "availability" -) - -type swarmOptions struct { - taskHistoryLimit int64 - dispatcherHeartbeat time.Duration - nodeCertExpiry time.Duration - externalCA ExternalCAOption - maxSnapshots uint64 - snapshotInterval uint64 - autolock bool -} - -// NodeAddrOption is a pflag.Value for listening addresses -type NodeAddrOption struct { - addr string -} - -// String prints the representation of this flag -func (a *NodeAddrOption) String() string { - return a.Value() -} - -// Set the value for this flag -func (a *NodeAddrOption) Set(value string) error { - addr, err := opts.ParseTCPAddr(value, a.addr) - if err != nil { - return err - } - a.addr = addr - return nil -} - -// Type returns the type of this flag -func (a *NodeAddrOption) Type() string { - return "node-addr" -} - -// Value returns the value of this option as addr:port -func (a *NodeAddrOption) Value() string { - return strings.TrimPrefix(a.addr, "tcp://") -} - -// NewNodeAddrOption returns a new node address option -func NewNodeAddrOption(addr string) NodeAddrOption { - return NodeAddrOption{addr} -} - -// NewListenAddrOption returns a NodeAddrOption with default values -func NewListenAddrOption() NodeAddrOption { - return NewNodeAddrOption(defaultListenAddr) -} - -// ExternalCAOption is a Value type for parsing external CA specifications. -type ExternalCAOption struct { - values []*swarm.ExternalCA -} - -// Set parses an external CA option. -func (m *ExternalCAOption) Set(value string) error { - parsed, err := parseExternalCA(value) - if err != nil { - return err - } - - m.values = append(m.values, parsed) - return nil -} - -// Type returns the type of this option. -func (m *ExternalCAOption) Type() string { - return "external-ca" -} - -// String returns a string repr of this option. -func (m *ExternalCAOption) String() string { - externalCAs := []string{} - for _, externalCA := range m.values { - repr := fmt.Sprintf("%s: %s", externalCA.Protocol, externalCA.URL) - externalCAs = append(externalCAs, repr) - } - return strings.Join(externalCAs, ", ") -} - -// Value returns the external CAs -func (m *ExternalCAOption) Value() []*swarm.ExternalCA { - return m.values -} - -// parseExternalCA parses an external CA specification from the command line, -// such as protocol=cfssl,url=https://example.com. -func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) { - csvReader := csv.NewReader(strings.NewReader(caSpec)) - fields, err := csvReader.Read() - if err != nil { - return nil, err - } - - externalCA := swarm.ExternalCA{ - Options: make(map[string]string), - } - - var ( - hasProtocol bool - hasURL bool - ) - - for _, field := range fields { - parts := strings.SplitN(field, "=", 2) - - if len(parts) != 2 { - return nil, errors.Errorf("invalid field '%s' must be a key=value pair", field) - } - - key, value := parts[0], parts[1] - - switch strings.ToLower(key) { - case "protocol": - hasProtocol = true - if strings.ToLower(value) == string(swarm.ExternalCAProtocolCFSSL) { - externalCA.Protocol = swarm.ExternalCAProtocolCFSSL - } else { - return nil, errors.Errorf("unrecognized external CA protocol %s", value) - } - case "url": - hasURL = true - externalCA.URL = value - case "cacert": - cacontents, err := ioutil.ReadFile(value) - if err != nil { - return nil, errors.Wrap(err, "unable to read CA cert for external CA") - } - if pemBlock, _ := pem.Decode(cacontents); pemBlock == nil { - return nil, errors.New("CA cert for external CA must be in PEM format") - } - externalCA.CACert = string(cacontents) - default: - externalCA.Options[key] = value - } - } - - if !hasProtocol { - return nil, errors.New("the external-ca option needs a protocol= parameter") - } - if !hasURL { - return nil, errors.New("the external-ca option needs a url= parameter") - } - - return &externalCA, nil -} - -func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) { - flags.Int64Var(&opts.taskHistoryLimit, flagTaskHistoryLimit, 5, "Task history retention limit") - flags.DurationVar(&opts.dispatcherHeartbeat, flagDispatcherHeartbeat, time.Duration(5*time.Second), "Dispatcher heartbeat period (ns|us|ms|s|m|h)") - flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates (ns|us|ms|s|m|h)") - flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints") - flags.Uint64Var(&opts.maxSnapshots, flagMaxSnapshots, 0, "Number of additional Raft snapshots to retain") - flags.SetAnnotation(flagMaxSnapshots, "version", []string{"1.25"}) - flags.Uint64Var(&opts.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots") - flags.SetAnnotation(flagSnapshotInterval, "version", []string{"1.25"}) -} - -func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) { - if flags.Changed(flagTaskHistoryLimit) { - spec.Orchestration.TaskHistoryRetentionLimit = &opts.taskHistoryLimit - } - if flags.Changed(flagDispatcherHeartbeat) { - spec.Dispatcher.HeartbeatPeriod = opts.dispatcherHeartbeat - } - if flags.Changed(flagCertExpiry) { - spec.CAConfig.NodeCertExpiry = opts.nodeCertExpiry - } - if flags.Changed(flagExternalCA) { - spec.CAConfig.ExternalCAs = opts.externalCA.Value() - } - if flags.Changed(flagMaxSnapshots) { - spec.Raft.KeepOldSnapshots = &opts.maxSnapshots - } - if flags.Changed(flagSnapshotInterval) { - spec.Raft.SnapshotInterval = opts.snapshotInterval - } - if flags.Changed(flagAutolock) { - spec.EncryptionConfig.AutoLockManagers = opts.autolock - } -} - -func (opts *swarmOptions) ToSpec(flags *pflag.FlagSet) swarm.Spec { - var spec swarm.Spec - opts.mergeSwarmSpec(&spec, flags) - return spec -} diff --git a/cli/command/swarm/opts_test.go b/cli/command/swarm/opts_test.go deleted file mode 100644 index c694cc1bdd..0000000000 --- a/cli/command/swarm/opts_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package swarm - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNodeAddrOptionSetHostAndPort(t *testing.T) { - opt := NewNodeAddrOption("old:123") - addr := "newhost:5555" - assert.NoError(t, opt.Set(addr)) - assert.Equal(t, addr, opt.Value()) -} - -func TestNodeAddrOptionSetHostOnly(t *testing.T) { - opt := NewListenAddrOption() - assert.NoError(t, opt.Set("newhost")) - assert.Equal(t, "newhost:2377", opt.Value()) -} - -func TestNodeAddrOptionSetHostOnlyIPv6(t *testing.T) { - opt := NewListenAddrOption() - assert.NoError(t, opt.Set("::1")) - assert.Equal(t, "[::1]:2377", opt.Value()) -} - -func TestNodeAddrOptionSetPortOnly(t *testing.T) { - opt := NewListenAddrOption() - assert.NoError(t, opt.Set(":4545")) - assert.Equal(t, "0.0.0.0:4545", opt.Value()) -} - -func TestNodeAddrOptionSetInvalidFormat(t *testing.T) { - opt := NewListenAddrOption() - assert.EqualError(t, opt.Set("http://localhost:4545"), "Invalid proto, expected tcp: http://localhost:4545") -} - -func TestExternalCAOptionErrors(t *testing.T) { - testCases := []struct { - externalCA string - expectedError string - }{ - { - externalCA: "", - expectedError: "EOF", - }, - { - externalCA: "anything", - expectedError: "invalid field 'anything' must be a key=value pair", - }, - { - externalCA: "foo=bar", - expectedError: "the external-ca option needs a protocol= parameter", - }, - { - externalCA: "protocol=baz", - expectedError: "unrecognized external CA protocol baz", - }, - { - externalCA: "protocol=cfssl", - expectedError: "the external-ca option needs a url= parameter", - }, - } - for _, tc := range testCases { - opt := &ExternalCAOption{} - assert.EqualError(t, opt.Set(tc.externalCA), tc.expectedError) - } -} - -func TestExternalCAOption(t *testing.T) { - testCases := []struct { - externalCA string - expected string - }{ - { - externalCA: "protocol=cfssl,url=anything", - expected: "cfssl: anything", - }, - { - externalCA: "protocol=CFSSL,url=anything", - expected: "cfssl: anything", - }, - { - externalCA: "protocol=Cfssl,url=https://example.com", - expected: "cfssl: https://example.com", - }, - { - externalCA: "protocol=Cfssl,url=https://example.com,foo=bar", - expected: "cfssl: https://example.com", - }, - { - externalCA: "protocol=Cfssl,url=https://example.com,foo=bar,foo=baz", - expected: "cfssl: https://example.com", - }, - } - for _, tc := range testCases { - opt := &ExternalCAOption{} - assert.NoError(t, opt.Set(tc.externalCA)) - assert.Equal(t, tc.expected, opt.String()) - } -} - -func TestExternalCAOptionMultiple(t *testing.T) { - opt := &ExternalCAOption{} - assert.NoError(t, opt.Set("protocol=cfssl,url=https://example.com")) - assert.NoError(t, opt.Set("protocol=CFSSL,url=anything")) - assert.Len(t, opt.Value(), 2) - assert.Equal(t, "cfssl: https://example.com, cfssl: anything", opt.String()) -} diff --git a/cli/command/swarm/testdata/init-init-autolock.golden b/cli/command/swarm/testdata/init-init-autolock.golden deleted file mode 100644 index cdd3c666b6..0000000000 --- a/cli/command/swarm/testdata/init-init-autolock.golden +++ /dev/null @@ -1,11 +0,0 @@ -Swarm initialized: current node (nodeID) is now a manager. - -To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions. - -To unlock a swarm manager after it restarts, run the `docker swarm unlock` -command and provide the following key: - - unlock-key - -Please remember to store this key in a password manager, since without it you -will not be able to restart the manager. diff --git a/cli/command/swarm/testdata/init-init.golden b/cli/command/swarm/testdata/init-init.golden deleted file mode 100644 index 6e82be010e..0000000000 --- a/cli/command/swarm/testdata/init-init.golden +++ /dev/null @@ -1,4 +0,0 @@ -Swarm initialized: current node (nodeID) is now a manager. - -To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions. - diff --git a/cli/command/swarm/testdata/jointoken-manager-quiet.golden b/cli/command/swarm/testdata/jointoken-manager-quiet.golden deleted file mode 100644 index 0c7cfc6088..0000000000 --- a/cli/command/swarm/testdata/jointoken-manager-quiet.golden +++ /dev/null @@ -1 +0,0 @@ -manager-join-token diff --git a/cli/command/swarm/testdata/jointoken-manager-rotate.golden b/cli/command/swarm/testdata/jointoken-manager-rotate.golden deleted file mode 100644 index b4d0a48f66..0000000000 --- a/cli/command/swarm/testdata/jointoken-manager-rotate.golden +++ /dev/null @@ -1,5 +0,0 @@ -Successfully rotated manager join token. - -To add a manager to this swarm, run the following command: - - docker swarm join --token manager-join-token 127.0.0.1 diff --git a/cli/command/swarm/testdata/jointoken-manager.golden b/cli/command/swarm/testdata/jointoken-manager.golden deleted file mode 100644 index 522b2968fe..0000000000 --- a/cli/command/swarm/testdata/jointoken-manager.golden +++ /dev/null @@ -1,3 +0,0 @@ -To add a manager to this swarm, run the following command: - - docker swarm join --token manager-join-token 127.0.0.1 diff --git a/cli/command/swarm/testdata/jointoken-worker-quiet.golden b/cli/command/swarm/testdata/jointoken-worker-quiet.golden deleted file mode 100644 index b445e191e5..0000000000 --- a/cli/command/swarm/testdata/jointoken-worker-quiet.golden +++ /dev/null @@ -1 +0,0 @@ -worker-join-token diff --git a/cli/command/swarm/testdata/jointoken-worker.golden b/cli/command/swarm/testdata/jointoken-worker.golden deleted file mode 100644 index 899df703fd..0000000000 --- a/cli/command/swarm/testdata/jointoken-worker.golden +++ /dev/null @@ -1,3 +0,0 @@ -To add a worker to this swarm, run the following command: - - docker swarm join --token worker-join-token 127.0.0.1 diff --git a/cli/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden b/cli/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden deleted file mode 100644 index ed53505e25..0000000000 --- a/cli/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden +++ /dev/null @@ -1 +0,0 @@ -unlock-key diff --git a/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden b/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden deleted file mode 100644 index ed53505e25..0000000000 --- a/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden +++ /dev/null @@ -1 +0,0 @@ -unlock-key diff --git a/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden b/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden deleted file mode 100644 index 89152b8643..0000000000 --- a/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden +++ /dev/null @@ -1,9 +0,0 @@ -Successfully rotated manager unlock key. - -To unlock a swarm manager after it restarts, run the `docker swarm unlock` -command and provide the following key: - - unlock-key - -Please remember to store this key in a password manager, since without it you -will not be able to restart the manager. diff --git a/cli/command/swarm/testdata/unlockkeys-unlock-key.golden b/cli/command/swarm/testdata/unlockkeys-unlock-key.golden deleted file mode 100644 index 8316df478c..0000000000 --- a/cli/command/swarm/testdata/unlockkeys-unlock-key.golden +++ /dev/null @@ -1,7 +0,0 @@ -To unlock a swarm manager after it restarts, run the `docker swarm unlock` -command and provide the following key: - - unlock-key - -Please remember to store this key in a password manager, since without it you -will not be able to restart the manager. diff --git a/cli/command/swarm/testdata/update-all-flags-quiet.golden b/cli/command/swarm/testdata/update-all-flags-quiet.golden deleted file mode 100644 index 3d195a2586..0000000000 --- a/cli/command/swarm/testdata/update-all-flags-quiet.golden +++ /dev/null @@ -1 +0,0 @@ -Swarm updated. diff --git a/cli/command/swarm/testdata/update-autolock-unlock-key.golden b/cli/command/swarm/testdata/update-autolock-unlock-key.golden deleted file mode 100644 index a077b9e167..0000000000 --- a/cli/command/swarm/testdata/update-autolock-unlock-key.golden +++ /dev/null @@ -1,8 +0,0 @@ -Swarm updated. -To unlock a swarm manager after it restarts, run the `docker swarm unlock` -command and provide the following key: - - unlock-key - -Please remember to store this key in a password manager, since without it you -will not be able to restart the manager. diff --git a/cli/command/swarm/testdata/update-noargs.golden b/cli/command/swarm/testdata/update-noargs.golden deleted file mode 100644 index 381c0ccf1f..0000000000 --- a/cli/command/swarm/testdata/update-noargs.golden +++ /dev/null @@ -1,13 +0,0 @@ -Update the swarm - -Usage: - update [OPTIONS] [flags] - -Flags: - --autolock Change manager autolocking setting (true|false) - --cert-expiry duration Validity period for node certificates (ns|us|ms|s|m|h) (default 2160h0m0s) - --dispatcher-heartbeat duration Dispatcher heartbeat period (ns|us|ms|s|m|h) (default 5s) - --external-ca external-ca Specifications of one or more certificate signing endpoints - --max-snapshots uint Number of additional Raft snapshots to retain - --snapshot-interval uint Number of log entries between Raft snapshots (default 10000) - --task-history-limit int Task history retention limit (default 5) diff --git a/cli/command/swarm/unlock.go b/cli/command/swarm/unlock.go deleted file mode 100644 index c1d9b99189..0000000000 --- a/cli/command/swarm/unlock.go +++ /dev/null @@ -1,78 +0,0 @@ -package swarm - -import ( - "bufio" - "fmt" - "io" - "strings" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/crypto/ssh/terminal" - "golang.org/x/net/context" -) - -type unlockOptions struct{} - -func newUnlockCommand(dockerCli command.Cli) *cobra.Command { - opts := unlockOptions{} - - cmd := &cobra.Command{ - Use: "unlock", - Short: "Unlock swarm", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runUnlock(dockerCli, opts) - }, - } - - return cmd -} - -func runUnlock(dockerCli command.Cli, opts unlockOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - // First see if the node is actually part of a swarm, and if it is actually locked first. - // If it's in any other state than locked, don't ask for the key. - info, err := client.Info(ctx) - if err != nil { - return err - } - - switch info.Swarm.LocalNodeState { - case swarm.LocalNodeStateInactive: - return errors.New("Error: This node is not part of a swarm") - case swarm.LocalNodeStateLocked: - break - default: - return errors.New("Error: swarm is not locked") - } - - key, err := readKey(dockerCli.In(), "Please enter unlock key: ") - if err != nil { - return err - } - req := swarm.UnlockRequest{ - UnlockKey: key, - } - - return client.SwarmUnlock(ctx, req) -} - -func readKey(in *command.InStream, prompt string) (string, error) { - if in.IsTerminal() { - fmt.Print(prompt) - dt, err := terminal.ReadPassword(int(in.FD())) - fmt.Println() - return string(dt), err - } - key, err := bufio.NewReader(in).ReadString('\n') - if err == io.EOF { - err = nil - } - return strings.TrimSpace(key), err -} diff --git a/cli/command/swarm/unlock_key.go b/cli/command/swarm/unlock_key.go deleted file mode 100644 index 77c97d88ea..0000000000 --- a/cli/command/swarm/unlock_key.go +++ /dev/null @@ -1,86 +0,0 @@ -package swarm - -import ( - "fmt" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type unlockKeyOptions struct { - rotate bool - quiet bool -} - -func newUnlockKeyCommand(dockerCli command.Cli) *cobra.Command { - opts := unlockKeyOptions{} - - cmd := &cobra.Command{ - Use: "unlock-key [OPTIONS]", - Short: "Manage the unlock key", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runUnlockKey(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVar(&opts.rotate, flagRotate, false, "Rotate unlock key") - flags.BoolVarP(&opts.quiet, flagQuiet, "q", false, "Only display token") - - return cmd -} - -func runUnlockKey(dockerCli command.Cli, opts unlockKeyOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - if opts.rotate { - flags := swarm.UpdateFlags{RotateManagerUnlockKey: true} - - sw, err := client.SwarmInspect(ctx) - if err != nil { - return err - } - - if !sw.Spec.EncryptionConfig.AutoLockManagers { - return errors.New("cannot rotate because autolock is not turned on") - } - - if err := client.SwarmUpdate(ctx, sw.Version, sw.Spec, flags); err != nil { - return err - } - - if !opts.quiet { - fmt.Fprintf(dockerCli.Out(), "Successfully rotated manager unlock key.\n\n") - } - } - - unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) - if err != nil { - return errors.Wrap(err, "could not fetch unlock key") - } - - if unlockKeyResp.UnlockKey == "" { - return errors.New("no unlock key is set") - } - - if opts.quiet { - fmt.Fprintln(dockerCli.Out(), unlockKeyResp.UnlockKey) - return nil - } - - printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey) - return nil -} - -func printUnlockCommand(ctx context.Context, dockerCli command.Cli, unlockKey string) { - if len(unlockKey) > 0 { - fmt.Fprintf(dockerCli.Out(), "To unlock a swarm manager after it restarts, run the `docker swarm unlock`\ncommand and provide the following key:\n\n %s\n\nPlease remember to store this key in a password manager, since without it you\nwill not be able to restart the manager.\n", unlockKey) - } - return -} diff --git a/cli/command/swarm/unlock_key_test.go b/cli/command/swarm/unlock_key_test.go deleted file mode 100644 index 23752104aa..0000000000 --- a/cli/command/swarm/unlock_key_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package swarm - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestSwarmUnlockKeyErrors(t *testing.T) { - testCases := []struct { - name string - args []string - flags map[string]string - swarmInspectFunc func() (swarm.Swarm, error) - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) - expectedError string - }{ - { - name: "too-many-args", - args: []string{"foo"}, - expectedError: "accepts no argument(s)", - }, - { - name: "swarm-inspect-rotate-failed", - flags: map[string]string{ - flagRotate: "true", - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, errors.Errorf("error inspecting the swarm") - }, - expectedError: "error inspecting the swarm", - }, - { - name: "swarm-rotate-no-autolock-failed", - flags: map[string]string{ - flagRotate: "true", - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(), nil - }, - expectedError: "cannot rotate because autolock is not turned on", - }, - { - name: "swarm-update-failed", - flags: map[string]string{ - flagRotate: "true", - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(Autolock()), nil - }, - swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - return errors.Errorf("error updating the swarm") - }, - expectedError: "error updating the swarm", - }, - { - name: "swarm-get-unlock-key-failed", - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{}, errors.Errorf("error getting unlock key") - }, - expectedError: "error getting unlock key", - }, - { - name: "swarm-no-unlock-key-failed", - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{ - UnlockKey: "", - }, nil - }, - expectedError: "no unlock key is set", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newUnlockKeyCommand( - test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - swarmUpdateFunc: tc.swarmUpdateFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSwarmUnlockKey(t *testing.T) { - testCases := []struct { - name string - args []string - flags map[string]string - swarmInspectFunc func() (swarm.Swarm, error) - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) - }{ - { - name: "unlock-key", - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{ - UnlockKey: "unlock-key", - }, nil - }, - }, - { - name: "unlock-key-quiet", - flags: map[string]string{ - flagQuiet: "true", - }, - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{ - UnlockKey: "unlock-key", - }, nil - }, - }, - { - name: "unlock-key-rotate", - flags: map[string]string{ - flagRotate: "true", - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(Autolock()), nil - }, - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{ - UnlockKey: "unlock-key", - }, nil - }, - }, - { - name: "unlock-key-rotate-quiet", - flags: map[string]string{ - flagQuiet: "true", - flagRotate: "true", - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(Autolock()), nil - }, - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{ - UnlockKey: "unlock-key", - }, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newUnlockKeyCommand( - test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - swarmUpdateFunc: tc.swarmUpdateFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("unlockkeys-%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} diff --git a/cli/command/swarm/unlock_test.go b/cli/command/swarm/unlock_test.go deleted file mode 100644 index 056c9746c6..0000000000 --- a/cli/command/swarm/unlock_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package swarm - -import ( - "bytes" - "io/ioutil" - "strings" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestSwarmUnlockErrors(t *testing.T) { - testCases := []struct { - name string - args []string - input string - swarmUnlockFunc func(req swarm.UnlockRequest) error - infoFunc func() (types.Info, error) - expectedError string - }{ - { - name: "too-many-args", - args: []string{"foo"}, - expectedError: "accepts no argument(s)", - }, - { - name: "is-not-part-of-a-swarm", - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - LocalNodeState: swarm.LocalNodeStateInactive, - }, - }, nil - }, - expectedError: "This node is not part of a swarm", - }, - { - name: "is-not-locked", - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - LocalNodeState: swarm.LocalNodeStateActive, - }, - }, nil - }, - expectedError: "Error: swarm is not locked", - }, - { - name: "unlockrequest-failed", - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - LocalNodeState: swarm.LocalNodeStateLocked, - }, - }, nil - }, - swarmUnlockFunc: func(req swarm.UnlockRequest) error { - return errors.Errorf("error unlocking the swarm") - }, - expectedError: "error unlocking the swarm", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newUnlockCommand( - test.NewFakeCli(&fakeClient{ - infoFunc: tc.infoFunc, - swarmUnlockFunc: tc.swarmUnlockFunc, - }, buf)) - cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSwarmUnlock(t *testing.T) { - input := "unlockKey" - buf := new(bytes.Buffer) - dockerCli := test.NewFakeCli(&fakeClient{ - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - LocalNodeState: swarm.LocalNodeStateLocked, - }, - }, nil - }, - swarmUnlockFunc: func(req swarm.UnlockRequest) error { - if req.UnlockKey != input { - return errors.Errorf("Invalid unlock key") - } - return nil - }, - }, buf) - dockerCli.SetIn(command.NewInStream(ioutil.NopCloser(strings.NewReader(input)))) - cmd := newUnlockCommand(dockerCli) - assert.NoError(t, cmd.Execute()) -} diff --git a/cli/command/swarm/update.go b/cli/command/swarm/update.go deleted file mode 100644 index 1ccd268e74..0000000000 --- a/cli/command/swarm/update.go +++ /dev/null @@ -1,72 +0,0 @@ -package swarm - -import ( - "fmt" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -func newUpdateCommand(dockerCli command.Cli) *cobra.Command { - opts := swarmOptions{} - - cmd := &cobra.Command{ - Use: "update [OPTIONS]", - Short: "Update the swarm", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runUpdate(dockerCli, cmd.Flags(), opts) - }, - PreRunE: func(cmd *cobra.Command, args []string) error { - if cmd.Flags().NFlag() == 0 { - return pflag.ErrHelp - } - return nil - }, - } - - cmd.Flags().BoolVar(&opts.autolock, flagAutolock, false, "Change manager autolocking setting (true|false)") - addSwarmFlags(cmd.Flags(), &opts) - return cmd -} - -func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, opts swarmOptions) error { - client := dockerCli.Client() - ctx := context.Background() - - var updateFlags swarm.UpdateFlags - - swarmInspect, err := client.SwarmInspect(ctx) - if err != nil { - return err - } - - prevAutoLock := swarmInspect.Spec.EncryptionConfig.AutoLockManagers - - opts.mergeSwarmSpec(&swarmInspect.Spec, flags) - - curAutoLock := swarmInspect.Spec.EncryptionConfig.AutoLockManagers - - err = client.SwarmUpdate(ctx, swarmInspect.Version, swarmInspect.Spec, updateFlags) - if err != nil { - return err - } - - fmt.Fprintln(dockerCli.Out(), "Swarm updated.") - - if curAutoLock && !prevAutoLock { - unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) - if err != nil { - return errors.Wrap(err, "could not fetch unlock key") - } - printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey) - } - - return nil -} diff --git a/cli/command/swarm/update_test.go b/cli/command/swarm/update_test.go deleted file mode 100644 index 65366ddd20..0000000000 --- a/cli/command/swarm/update_test.go +++ /dev/null @@ -1,184 +0,0 @@ -package swarm - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestSwarmUpdateErrors(t *testing.T) { - testCases := []struct { - name string - args []string - flags map[string]string - swarmInspectFunc func() (swarm.Swarm, error) - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) - expectedError string - }{ - { - name: "too-many-args", - args: []string{"foo"}, - expectedError: "accepts no argument(s)", - }, - { - name: "swarm-inspect-error", - flags: map[string]string{ - flagTaskHistoryLimit: "10", - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, errors.Errorf("error inspecting the swarm") - }, - expectedError: "error inspecting the swarm", - }, - { - name: "swarm-update-error", - flags: map[string]string{ - flagTaskHistoryLimit: "10", - }, - swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - return errors.Errorf("error updating the swarm") - }, - expectedError: "error updating the swarm", - }, - { - name: "swarm-unlockkey-error", - flags: map[string]string{ - flagAutolock: "true", - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(), nil - }, - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{}, errors.Errorf("error getting unlock key") - }, - expectedError: "error getting unlock key", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newUpdateCommand( - test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - swarmUpdateFunc: tc.swarmUpdateFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestSwarmUpdate(t *testing.T) { - testCases := []struct { - name string - args []string - flags map[string]string - swarmInspectFunc func() (swarm.Swarm, error) - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) - }{ - { - name: "noargs", - }, - { - name: "all-flags-quiet", - flags: map[string]string{ - flagTaskHistoryLimit: "10", - flagDispatcherHeartbeat: "10s", - flagCertExpiry: "20s", - flagExternalCA: "protocol=cfssl,url=https://example.com.", - flagMaxSnapshots: "10", - flagSnapshotInterval: "100", - flagAutolock: "true", - flagQuiet: "true", - }, - swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - if *swarm.Orchestration.TaskHistoryRetentionLimit != 10 { - return errors.Errorf("historyLimit not correctly set") - } - heartbeatDuration, err := time.ParseDuration("10s") - if err != nil { - return err - } - if swarm.Dispatcher.HeartbeatPeriod != heartbeatDuration { - return errors.Errorf("heartbeatPeriodLimit not correctly set") - } - certExpiryDuration, err := time.ParseDuration("20s") - if err != nil { - return err - } - if swarm.CAConfig.NodeCertExpiry != certExpiryDuration { - return errors.Errorf("certExpiry not correctly set") - } - if len(swarm.CAConfig.ExternalCAs) != 1 { - return errors.Errorf("externalCA not correctly set") - } - if *swarm.Raft.KeepOldSnapshots != 10 { - return errors.Errorf("keepOldSnapshots not correctly set") - } - if swarm.Raft.SnapshotInterval != 100 { - return errors.Errorf("snapshotInterval not correctly set") - } - if !swarm.EncryptionConfig.AutoLockManagers { - return errors.Errorf("autolock not correctly set") - } - return nil - }, - }, - { - name: "autolock-unlock-key", - flags: map[string]string{ - flagTaskHistoryLimit: "10", - flagAutolock: "true", - }, - swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - if *swarm.Orchestration.TaskHistoryRetentionLimit != 10 { - return errors.Errorf("historyLimit not correctly set") - } - return nil - }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *Swarm(), nil - }, - swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{ - UnlockKey: "unlock-key", - }, nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newUpdateCommand( - test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - swarmUpdateFunc: tc.swarmUpdateFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, - }, buf)) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(buf) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("update-%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} diff --git a/cli/command/system/cmd.go b/cli/command/system/cmd.go deleted file mode 100644 index ab3beb895a..0000000000 --- a/cli/command/system/cmd.go +++ /dev/null @@ -1,26 +0,0 @@ -package system - -import ( - "github.com/spf13/cobra" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" -) - -// NewSystemCommand returns a cobra command for `system` subcommands -func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "system", - Short: "Manage Docker", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - } - cmd.AddCommand( - NewEventsCommand(dockerCli), - NewInfoCommand(dockerCli), - NewDiskUsageCommand(dockerCli), - NewPruneCommand(dockerCli), - ) - - return cmd -} diff --git a/cli/command/system/df.go b/cli/command/system/df.go deleted file mode 100644 index 67b3b31d87..0000000000 --- a/cli/command/system/df.go +++ /dev/null @@ -1,68 +0,0 @@ -package system - -import ( - "errors" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type diskUsageOptions struct { - verbose bool - format string -} - -// NewDiskUsageCommand creates a new cobra.Command for `docker df` -func NewDiskUsageCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts diskUsageOptions - - cmd := &cobra.Command{ - Use: "df [OPTIONS]", - Short: "Show docker disk usage", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runDiskUsage(dockerCli, opts) - }, - Tags: map[string]string{"version": "1.25"}, - } - - flags := cmd.Flags() - - flags.BoolVarP(&opts.verbose, "verbose", "v", false, "Show detailed information on space usage") - flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template") - - return cmd -} - -func runDiskUsage(dockerCli *command.DockerCli, opts diskUsageOptions) error { - if opts.verbose && len(opts.format) != 0 { - return errors.New("the verbose and the format options conflict") - } - - du, err := dockerCli.Client().DiskUsage(context.Background()) - if err != nil { - return err - } - - format := opts.format - if len(format) == 0 { - format = formatter.TableFormatKey - } - - duCtx := formatter.DiskUsageContext{ - Context: formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewDiskUsageFormat(format), - }, - LayersSize: du.LayersSize, - Images: du.Images, - Containers: du.Containers, - Volumes: du.Volumes, - Verbose: opts.verbose, - } - - return duCtx.Write() -} diff --git a/cli/command/system/events.go b/cli/command/system/events.go deleted file mode 100644 index 441ef91d33..0000000000 --- a/cli/command/system/events.go +++ /dev/null @@ -1,140 +0,0 @@ -package system - -import ( - "fmt" - "io" - "io/ioutil" - "sort" - "strings" - "text/template" - "time" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - eventtypes "github.com/docker/docker/api/types/events" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/jsonlog" - "github.com/docker/docker/pkg/templates" - "github.com/spf13/cobra" -) - -type eventsOptions struct { - since string - until string - filter opts.FilterOpt - format string -} - -// NewEventsCommand creates a new cobra.Command for `docker events` -func NewEventsCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := eventsOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "events [OPTIONS]", - Short: "Get real time events from the server", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runEvents(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - flags.StringVar(&opts.since, "since", "", "Show all events created since timestamp") - flags.StringVar(&opts.until, "until", "", "Stream events until this timestamp") - flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - flags.StringVar(&opts.format, "format", "", "Format the output using the given Go template") - - return cmd -} - -func runEvents(dockerCli *command.DockerCli, opts *eventsOptions) error { - tmpl, err := makeTemplate(opts.format) - if err != nil { - return cli.StatusError{ - StatusCode: 64, - Status: "Error parsing format: " + err.Error()} - } - options := types.EventsOptions{ - Since: opts.since, - Until: opts.until, - Filters: opts.filter.Value(), - } - - ctx, cancel := context.WithCancel(context.Background()) - events, errs := dockerCli.Client().Events(ctx, options) - defer cancel() - - out := dockerCli.Out() - - for { - select { - case event := <-events: - if err := handleEvent(out, event, tmpl); err != nil { - return err - } - case err := <-errs: - if err == io.EOF { - return nil - } - return err - } - } -} - -func handleEvent(out io.Writer, event eventtypes.Message, tmpl *template.Template) error { - if tmpl == nil { - return prettyPrintEvent(out, event) - } - - return formatEvent(out, event, tmpl) -} - -func makeTemplate(format string) (*template.Template, error) { - if format == "" { - return nil, nil - } - tmpl, err := templates.Parse(format) - if err != nil { - return tmpl, err - } - // we execute the template for an empty message, so as to validate - // a bad template like "{{.badFieldString}}" - return tmpl, tmpl.Execute(ioutil.Discard, &eventtypes.Message{}) -} - -// prettyPrintEvent prints all types of event information. -// Each output includes the event type, actor id, name and action. -// Actor attributes are printed at the end if the actor has any. -func prettyPrintEvent(out io.Writer, event eventtypes.Message) error { - if event.TimeNano != 0 { - fmt.Fprintf(out, "%s ", time.Unix(0, event.TimeNano).Format(jsonlog.RFC3339NanoFixed)) - } else if event.Time != 0 { - fmt.Fprintf(out, "%s ", time.Unix(event.Time, 0).Format(jsonlog.RFC3339NanoFixed)) - } - - fmt.Fprintf(out, "%s %s %s", event.Type, event.Action, event.Actor.ID) - - if len(event.Actor.Attributes) > 0 { - var attrs []string - var keys []string - for k := range event.Actor.Attributes { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - v := event.Actor.Attributes[k] - attrs = append(attrs, fmt.Sprintf("%s=%s", k, v)) - } - fmt.Fprintf(out, " (%s)", strings.Join(attrs, ", ")) - } - fmt.Fprint(out, "\n") - return nil -} - -func formatEvent(out io.Writer, event eventtypes.Message, tmpl *template.Template) error { - defer out.Write([]byte{'\n'}) - return tmpl.Execute(out, event) -} diff --git a/cli/command/system/info.go b/cli/command/system/info.go deleted file mode 100644 index 8ded8124dc..0000000000 --- a/cli/command/system/info.go +++ /dev/null @@ -1,369 +0,0 @@ -package system - -import ( - "fmt" - "sort" - "strings" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/debug" - "github.com/docker/docker/pkg/ioutils" - "github.com/docker/docker/pkg/templates" - "github.com/docker/go-units" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type infoOptions struct { - format string -} - -// NewInfoCommand creates a new cobra.Command for `docker info` -func NewInfoCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts infoOptions - - cmd := &cobra.Command{ - Use: "info [OPTIONS]", - Short: "Display system-wide information", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runInfo(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - - return cmd -} - -func runInfo(dockerCli *command.DockerCli, opts *infoOptions) error { - ctx := context.Background() - info, err := dockerCli.Client().Info(ctx) - if err != nil { - return err - } - if opts.format == "" { - return prettyPrintInfo(dockerCli, info) - } - return formatInfo(dockerCli, info, opts.format) -} - -func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { - fmt.Fprintf(dockerCli.Out(), "Containers: %d\n", info.Containers) - fmt.Fprintf(dockerCli.Out(), " Running: %d\n", info.ContainersRunning) - fmt.Fprintf(dockerCli.Out(), " Paused: %d\n", info.ContainersPaused) - fmt.Fprintf(dockerCli.Out(), " Stopped: %d\n", info.ContainersStopped) - fmt.Fprintf(dockerCli.Out(), "Images: %d\n", info.Images) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Server Version: %s\n", info.ServerVersion) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Storage Driver: %s\n", info.Driver) - if info.DriverStatus != nil { - for _, pair := range info.DriverStatus { - fmt.Fprintf(dockerCli.Out(), " %s: %s\n", pair[0], pair[1]) - } - - } - if info.SystemStatus != nil { - for _, pair := range info.SystemStatus { - fmt.Fprintf(dockerCli.Out(), "%s: %s\n", pair[0], pair[1]) - } - } - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Logging Driver: %s\n", info.LoggingDriver) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Cgroup Driver: %s\n", info.CgroupDriver) - - fmt.Fprintf(dockerCli.Out(), "Plugins: \n") - fmt.Fprintf(dockerCli.Out(), " Volume:") - fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Volume, " ")) - fmt.Fprintf(dockerCli.Out(), "\n") - fmt.Fprintf(dockerCli.Out(), " Network:") - fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Network, " ")) - fmt.Fprintf(dockerCli.Out(), "\n") - - if len(info.Plugins.Authorization) != 0 { - fmt.Fprintf(dockerCli.Out(), " Authorization:") - fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Authorization, " ")) - fmt.Fprintf(dockerCli.Out(), "\n") - } - - fmt.Fprintf(dockerCli.Out(), " Log:") - fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Log, " ")) - fmt.Fprintf(dockerCli.Out(), "\n") - - fmt.Fprintf(dockerCli.Out(), "Swarm: %v\n", info.Swarm.LocalNodeState) - if info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive && info.Swarm.LocalNodeState != swarm.LocalNodeStateLocked { - fmt.Fprintf(dockerCli.Out(), " NodeID: %s\n", info.Swarm.NodeID) - if info.Swarm.Error != "" { - fmt.Fprintf(dockerCli.Out(), " Error: %v\n", info.Swarm.Error) - } - fmt.Fprintf(dockerCli.Out(), " Is Manager: %v\n", info.Swarm.ControlAvailable) - if info.Swarm.Cluster != nil && info.Swarm.ControlAvailable && info.Swarm.Error == "" && info.Swarm.LocalNodeState != swarm.LocalNodeStateError { - fmt.Fprintf(dockerCli.Out(), " ClusterID: %s\n", info.Swarm.Cluster.ID) - fmt.Fprintf(dockerCli.Out(), " Managers: %d\n", info.Swarm.Managers) - fmt.Fprintf(dockerCli.Out(), " Nodes: %d\n", info.Swarm.Nodes) - fmt.Fprintf(dockerCli.Out(), " Orchestration:\n") - taskHistoryRetentionLimit := int64(0) - if info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit != nil { - taskHistoryRetentionLimit = *info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit - } - fmt.Fprintf(dockerCli.Out(), " Task History Retention Limit: %d\n", taskHistoryRetentionLimit) - fmt.Fprintf(dockerCli.Out(), " Raft:\n") - fmt.Fprintf(dockerCli.Out(), " Snapshot Interval: %d\n", info.Swarm.Cluster.Spec.Raft.SnapshotInterval) - if info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots != nil { - fmt.Fprintf(dockerCli.Out(), " Number of Old Snapshots to Retain: %d\n", *info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots) - } - fmt.Fprintf(dockerCli.Out(), " Heartbeat Tick: %d\n", info.Swarm.Cluster.Spec.Raft.HeartbeatTick) - fmt.Fprintf(dockerCli.Out(), " Election Tick: %d\n", info.Swarm.Cluster.Spec.Raft.ElectionTick) - fmt.Fprintf(dockerCli.Out(), " Dispatcher:\n") - fmt.Fprintf(dockerCli.Out(), " Heartbeat Period: %s\n", units.HumanDuration(time.Duration(info.Swarm.Cluster.Spec.Dispatcher.HeartbeatPeriod))) - fmt.Fprintf(dockerCli.Out(), " CA Configuration:\n") - fmt.Fprintf(dockerCli.Out(), " Expiry Duration: %s\n", units.HumanDuration(info.Swarm.Cluster.Spec.CAConfig.NodeCertExpiry)) - if len(info.Swarm.Cluster.Spec.CAConfig.ExternalCAs) > 0 { - fmt.Fprintf(dockerCli.Out(), " External CAs:\n") - for _, entry := range info.Swarm.Cluster.Spec.CAConfig.ExternalCAs { - fmt.Fprintf(dockerCli.Out(), " %s: %s\n", entry.Protocol, entry.URL) - } - } - } - fmt.Fprintf(dockerCli.Out(), " Node Address: %s\n", info.Swarm.NodeAddr) - managers := []string{} - for _, entry := range info.Swarm.RemoteManagers { - managers = append(managers, entry.Addr) - } - if len(managers) > 0 { - sort.Strings(managers) - fmt.Fprintf(dockerCli.Out(), " Manager Addresses:\n") - for _, entry := range managers { - fmt.Fprintf(dockerCli.Out(), " %s\n", entry) - } - } - } - - if len(info.Runtimes) > 0 { - fmt.Fprintf(dockerCli.Out(), "Runtimes:") - for name := range info.Runtimes { - fmt.Fprintf(dockerCli.Out(), " %s", name) - } - fmt.Fprint(dockerCli.Out(), "\n") - fmt.Fprintf(dockerCli.Out(), "Default Runtime: %s\n", info.DefaultRuntime) - } - - if info.OSType == "linux" { - fmt.Fprintf(dockerCli.Out(), "Init Binary: %v\n", info.InitBinary) - - for _, ci := range []struct { - Name string - Commit types.Commit - }{ - {"containerd", info.ContainerdCommit}, - {"runc", info.RuncCommit}, - {"init", info.InitCommit}, - } { - fmt.Fprintf(dockerCli.Out(), "%s version: %s", ci.Name, ci.Commit.ID) - if ci.Commit.ID != ci.Commit.Expected { - fmt.Fprintf(dockerCli.Out(), " (expected: %s)", ci.Commit.Expected) - } - fmt.Fprintf(dockerCli.Out(), "\n") - } - if len(info.SecurityOptions) != 0 { - kvs, err := types.DecodeSecurityOptions(info.SecurityOptions) - if err != nil { - return err - } - fmt.Fprintf(dockerCli.Out(), "Security Options:\n") - for _, so := range kvs { - fmt.Fprintf(dockerCli.Out(), " %s\n", so.Name) - for _, o := range so.Options { - switch o.Key { - case "profile": - if o.Value != "default" { - fmt.Fprintf(dockerCli.Err(), " WARNING: You're not using the default seccomp profile\n") - } - fmt.Fprintf(dockerCli.Out(), " Profile: %s\n", o.Value) - } - } - } - } - } - - // Isolation only has meaning on a Windows daemon. - if info.OSType == "windows" { - fmt.Fprintf(dockerCli.Out(), "Default Isolation: %v\n", info.Isolation) - } - - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Kernel Version: %s\n", info.KernelVersion) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Operating System: %s\n", info.OperatingSystem) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "OSType: %s\n", info.OSType) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Architecture: %s\n", info.Architecture) - fmt.Fprintf(dockerCli.Out(), "CPUs: %d\n", info.NCPU) - fmt.Fprintf(dockerCli.Out(), "Total Memory: %s\n", units.BytesSize(float64(info.MemTotal))) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Name: %s\n", info.Name) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "ID: %s\n", info.ID) - fmt.Fprintf(dockerCli.Out(), "Docker Root Dir: %s\n", info.DockerRootDir) - fmt.Fprintf(dockerCli.Out(), "Debug Mode (client): %v\n", debug.IsEnabled()) - fmt.Fprintf(dockerCli.Out(), "Debug Mode (server): %v\n", info.Debug) - - if info.Debug { - fmt.Fprintf(dockerCli.Out(), " File Descriptors: %d\n", info.NFd) - fmt.Fprintf(dockerCli.Out(), " Goroutines: %d\n", info.NGoroutines) - fmt.Fprintf(dockerCli.Out(), " System Time: %s\n", info.SystemTime) - fmt.Fprintf(dockerCli.Out(), " EventsListeners: %d\n", info.NEventsListener) - } - - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Http Proxy: %s\n", info.HTTPProxy) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Https Proxy: %s\n", info.HTTPSProxy) - ioutils.FprintfIfNotEmpty(dockerCli.Out(), "No Proxy: %s\n", info.NoProxy) - - if info.IndexServerAddress != "" { - u := dockerCli.ConfigFile().AuthConfigs[info.IndexServerAddress].Username - if len(u) > 0 { - fmt.Fprintf(dockerCli.Out(), "Username: %v\n", u) - } - fmt.Fprintf(dockerCli.Out(), "Registry: %v\n", info.IndexServerAddress) - } - - if info.Labels != nil { - fmt.Fprintln(dockerCli.Out(), "Labels:") - for _, attribute := range info.Labels { - fmt.Fprintf(dockerCli.Out(), " %s\n", attribute) - } - // TODO: Engine labels with duplicate keys has been deprecated in 1.13 and will be error out - // after 3 release cycles (17.12). For now, a WARNING will be generated. The following will - // be removed eventually. - labelMap := map[string]string{} - for _, label := range info.Labels { - stringSlice := strings.SplitN(label, "=", 2) - if len(stringSlice) > 1 { - // If there is a conflict we will throw out a warning - if v, ok := labelMap[stringSlice[0]]; ok && v != stringSlice[1] { - fmt.Fprintln(dockerCli.Err(), "WARNING: labels with duplicate keys and conflicting values have been deprecated") - break - } - labelMap[stringSlice[0]] = stringSlice[1] - } - } - } - - fmt.Fprintf(dockerCli.Out(), "Experimental: %v\n", info.ExperimentalBuild) - if info.ClusterStore != "" { - fmt.Fprintf(dockerCli.Out(), "Cluster Store: %s\n", info.ClusterStore) - } - - if info.ClusterAdvertise != "" { - fmt.Fprintf(dockerCli.Out(), "Cluster Advertise: %s\n", info.ClusterAdvertise) - } - - if info.RegistryConfig != nil && (len(info.RegistryConfig.InsecureRegistryCIDRs) > 0 || len(info.RegistryConfig.IndexConfigs) > 0) { - fmt.Fprintln(dockerCli.Out(), "Insecure Registries:") - for _, registry := range info.RegistryConfig.IndexConfigs { - if registry.Secure == false { - fmt.Fprintf(dockerCli.Out(), " %s\n", registry.Name) - } - } - - for _, registry := range info.RegistryConfig.InsecureRegistryCIDRs { - mask, _ := registry.Mask.Size() - fmt.Fprintf(dockerCli.Out(), " %s/%d\n", registry.IP.String(), mask) - } - } - - if info.RegistryConfig != nil && len(info.RegistryConfig.Mirrors) > 0 { - fmt.Fprintln(dockerCli.Out(), "Registry Mirrors:") - for _, mirror := range info.RegistryConfig.Mirrors { - fmt.Fprintf(dockerCli.Out(), " %s\n", mirror) - } - } - - fmt.Fprintf(dockerCli.Out(), "Live Restore Enabled: %v\n\n", info.LiveRestoreEnabled) - - // Only output these warnings if the server does not support these features - if info.OSType != "windows" { - printStorageDriverWarnings(dockerCli, info) - - if !info.MemoryLimit { - fmt.Fprintln(dockerCli.Err(), "WARNING: No memory limit support") - } - if !info.SwapLimit { - fmt.Fprintln(dockerCli.Err(), "WARNING: No swap limit support") - } - if !info.KernelMemory { - fmt.Fprintln(dockerCli.Err(), "WARNING: No kernel memory limit support") - } - if !info.OomKillDisable { - fmt.Fprintln(dockerCli.Err(), "WARNING: No oom kill disable support") - } - if !info.CPUCfsQuota { - fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs quota support") - } - if !info.CPUCfsPeriod { - fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs period support") - } - if !info.CPUShares { - fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu shares support") - } - if !info.CPUSet { - fmt.Fprintln(dockerCli.Err(), "WARNING: No cpuset support") - } - if !info.IPv4Forwarding { - fmt.Fprintln(dockerCli.Err(), "WARNING: IPv4 forwarding is disabled") - } - if !info.BridgeNfIptables { - fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-iptables is disabled") - } - if !info.BridgeNfIP6tables { - fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-ip6tables is disabled") - } - } - - return nil -} - -func printStorageDriverWarnings(dockerCli *command.DockerCli, info types.Info) { - if info.DriverStatus == nil { - return - } - - for _, pair := range info.DriverStatus { - if pair[0] == "Data loop file" { - fmt.Fprintf(dockerCli.Err(), "WARNING: %s: usage of loopback devices is strongly discouraged for production use.\n Use `--storage-opt dm.thinpooldev` to specify a custom block storage device.\n", info.Driver) - } - if pair[0] == "Supports d_type" && pair[1] == "false" { - backingFs := getBackingFs(info) - - msg := fmt.Sprintf("WARNING: %s: the backing %s filesystem is formatted without d_type support, which leads to incorrect behavior.\n", info.Driver, backingFs) - if backingFs == "xfs" { - msg += " Reformat the filesystem with ftype=1 to enable d_type support.\n" - } - msg += " Running without d_type support will not be supported in future releases." - fmt.Fprintln(dockerCli.Err(), msg) - } - } -} - -func getBackingFs(info types.Info) string { - if info.DriverStatus == nil { - return "" - } - - for _, pair := range info.DriverStatus { - if pair[0] == "Backing Filesystem" { - return pair[1] - } - } - return "" -} - -func formatInfo(dockerCli *command.DockerCli, info types.Info, format string) error { - tmpl, err := templates.Parse(format) - if err != nil { - return cli.StatusError{StatusCode: 64, - Status: "Template parsing error: " + err.Error()} - } - err = tmpl.Execute(dockerCli.Out(), info) - dockerCli.Out().Write([]byte{'\n'}) - return err -} diff --git a/cli/command/system/inspect.go b/cli/command/system/inspect.go deleted file mode 100644 index ad23d35a09..0000000000 --- a/cli/command/system/inspect.go +++ /dev/null @@ -1,216 +0,0 @@ -package system - -import ( - "fmt" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" - apiclient "github.com/docker/docker/client" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type inspectOptions struct { - format string - inspectType string - size bool - ids []string -} - -// NewInspectCommand creates a new cobra.Command for `docker inspect` -func NewInspectCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts inspectOptions - - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] NAME|ID [NAME|ID...]", - Short: "Return low-level information on Docker objects", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.ids = args - return runInspect(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - flags.StringVar(&opts.inspectType, "type", "", "Return JSON for specified type") - flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes if the type is container") - - return cmd -} - -func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { - var elementSearcher inspect.GetRefFunc - switch opts.inspectType { - case "", "container", "image", "node", "network", "service", "volume", "task", "plugin", "secret": - elementSearcher = inspectAll(context.Background(), dockerCli, opts.size, opts.inspectType) - default: - return errors.Errorf("%q is not a valid value for --type", opts.inspectType) - } - return inspect.Inspect(dockerCli.Out(), opts.ids, opts.format, elementSearcher) -} - -func inspectContainers(ctx context.Context, dockerCli *command.DockerCli, getSize bool) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().ContainerInspectWithRaw(ctx, ref, getSize) - } -} - -func inspectImages(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().ImageInspectWithRaw(ctx, ref) - } -} - -func inspectNetwork(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().NetworkInspectWithRaw(ctx, ref, false) - } -} - -func inspectNode(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().NodeInspectWithRaw(ctx, ref) - } -} - -func inspectService(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - // Service inspect shows defaults values in empty fields. - return dockerCli.Client().ServiceInspectWithRaw(ctx, ref, types.ServiceInspectOptions{InsertDefaults: true}) - } -} - -func inspectTasks(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().TaskInspectWithRaw(ctx, ref) - } -} - -func inspectVolume(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().VolumeInspectWithRaw(ctx, ref) - } -} - -func inspectPlugin(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().PluginInspectWithRaw(ctx, ref) - } -} - -func inspectSecret(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { - return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().SecretInspectWithRaw(ctx, ref) - } -} - -func inspectAll(ctx context.Context, dockerCli *command.DockerCli, getSize bool, typeConstraint string) inspect.GetRefFunc { - var inspectAutodetect = []struct { - objectType string - isSizeSupported bool - isSwarmObject bool - objectInspector func(string) (interface{}, []byte, error) - }{ - { - objectType: "container", - isSizeSupported: true, - objectInspector: inspectContainers(ctx, dockerCli, getSize), - }, - { - objectType: "image", - objectInspector: inspectImages(ctx, dockerCli), - }, - { - objectType: "network", - objectInspector: inspectNetwork(ctx, dockerCli), - }, - { - objectType: "volume", - objectInspector: inspectVolume(ctx, dockerCli), - }, - { - objectType: "service", - isSwarmObject: true, - objectInspector: inspectService(ctx, dockerCli), - }, - { - objectType: "task", - isSwarmObject: true, - objectInspector: inspectTasks(ctx, dockerCli), - }, - { - objectType: "node", - isSwarmObject: true, - objectInspector: inspectNode(ctx, dockerCli), - }, - { - objectType: "plugin", - objectInspector: inspectPlugin(ctx, dockerCli), - }, - { - objectType: "secret", - isSwarmObject: true, - objectInspector: inspectSecret(ctx, dockerCli), - }, - } - - // isSwarmManager does an Info API call to verify that the daemon is - // a swarm manager. - isSwarmManager := func() bool { - info, err := dockerCli.Client().Info(ctx) - if err != nil { - fmt.Fprintln(dockerCli.Err(), err) - return false - } - return info.Swarm.ControlAvailable - } - - isErrNotSupported := func(err error) bool { - return strings.Contains(err.Error(), "not supported") - } - - return func(ref string) (interface{}, []byte, error) { - const ( - swarmSupportUnknown = iota - swarmSupported - swarmUnsupported - ) - - isSwarmSupported := swarmSupportUnknown - - for _, inspectData := range inspectAutodetect { - if typeConstraint != "" && inspectData.objectType != typeConstraint { - continue - } - if typeConstraint == "" && inspectData.isSwarmObject { - if isSwarmSupported == swarmSupportUnknown { - if isSwarmManager() { - isSwarmSupported = swarmSupported - } else { - isSwarmSupported = swarmUnsupported - } - } - if isSwarmSupported == swarmUnsupported { - continue - } - } - v, raw, err := inspectData.objectInspector(ref) - if err != nil { - if typeConstraint == "" && (apiclient.IsErrNotFound(err) || isErrNotSupported(err)) { - continue - } - return v, raw, err - } - if getSize && !inspectData.isSizeSupported { - fmt.Fprintf(dockerCli.Err(), "WARNING: --size ignored for %s\n", inspectData.objectType) - } - return v, raw, err - } - return nil, nil, errors.Errorf("Error: No such object: %s", ref) - } -} diff --git a/cli/command/system/prune.go b/cli/command/system/prune.go deleted file mode 100644 index 46e4316f4a..0000000000 --- a/cli/command/system/prune.go +++ /dev/null @@ -1,96 +0,0 @@ -package system - -import ( - "fmt" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/prune" - "github.com/docker/docker/opts" - units "github.com/docker/go-units" - "github.com/spf13/cobra" -) - -type pruneOptions struct { - force bool - all bool - filter opts.FilterOpt -} - -// NewPruneCommand creates a new cobra.Command for `docker prune` -func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := pruneOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "prune [OPTIONS]", - Short: "Remove unused data", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runPrune(dockerCli, opts) - }, - Tags: map[string]string{"version": "1.25"}, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") - flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images not just dangling ones") - flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=')") - - return cmd -} - -const ( - warning = `WARNING! This will remove: - - all stopped containers - - all volumes not used by at least one container - - all networks not used by at least one container - %s -Are you sure you want to continue?` - - danglingImageDesc = "- all dangling images" - allImageDesc = `- all images without at least one container associated to them` -) - -func runPrune(dockerCli *command.DockerCli, options pruneOptions) error { - var message string - - if options.all { - message = fmt.Sprintf(warning, allImageDesc) - } else { - message = fmt.Sprintf(warning, danglingImageDesc) - } - - if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), message) { - return nil - } - - var spaceReclaimed uint64 - - for _, pruneFn := range []func(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error){ - prune.RunContainerPrune, - prune.RunVolumePrune, - prune.RunNetworkPrune, - } { - spc, output, err := pruneFn(dockerCli, options.filter) - if err != nil { - return err - } - spaceReclaimed += spc - if output != "" { - fmt.Fprintln(dockerCli.Out(), output) - } - } - - spc, output, err := prune.RunImagePrune(dockerCli, options.all, options.filter) - if err != nil { - return err - } - if spc > 0 { - spaceReclaimed += spc - fmt.Fprintln(dockerCli.Out(), output) - } - - fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) - - return nil -} diff --git a/cli/command/system/version.go b/cli/command/system/version.go deleted file mode 100644 index 468db7d03a..0000000000 --- a/cli/command/system/version.go +++ /dev/null @@ -1,131 +0,0 @@ -package system - -import ( - "runtime" - "time" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/dockerversion" - "github.com/docker/docker/pkg/templates" - "github.com/spf13/cobra" -) - -var versionTemplate = `Client: - Version: {{.Client.Version}} - API version: {{.Client.APIVersion}}{{if ne .Client.APIVersion .Client.DefaultAPIVersion}} (downgraded from {{.Client.DefaultAPIVersion}}){{end}} - Go version: {{.Client.GoVersion}} - Git commit: {{.Client.GitCommit}} - Built: {{.Client.BuildTime}} - OS/Arch: {{.Client.Os}}/{{.Client.Arch}}{{if .ServerOK}} - -Server: - Version: {{.Server.Version}} - API version: {{.Server.APIVersion}} (minimum version {{.Server.MinAPIVersion}}) - Go version: {{.Server.GoVersion}} - Git commit: {{.Server.GitCommit}} - Built: {{.Server.BuildTime}} - OS/Arch: {{.Server.Os}}/{{.Server.Arch}} - Experimental: {{.Server.Experimental}}{{end}}` - -type versionOptions struct { - format string -} - -// versionInfo contains version information of both the Client, and Server -type versionInfo struct { - Client clientVersion - Server *types.Version -} - -type clientVersion struct { - Version string - APIVersion string `json:"ApiVersion"` - DefaultAPIVersion string `json:"DefaultAPIVersion,omitempty"` - GitCommit string - GoVersion string - Os string - Arch string - BuildTime string `json:",omitempty"` -} - -// ServerOK returns true when the client could connect to the docker server -// and parse the information received. It returns false otherwise. -func (v versionInfo) ServerOK() bool { - return v.Server != nil -} - -// NewVersionCommand creates a new cobra.Command for `docker version` -func NewVersionCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts versionOptions - - cmd := &cobra.Command{ - Use: "version [OPTIONS]", - Short: "Show the Docker version information", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runVersion(dockerCli, &opts) - }, - } - - flags := cmd.Flags() - - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - - return cmd -} - -func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error { - ctx := context.Background() - - templateFormat := versionTemplate - if opts.format != "" { - templateFormat = opts.format - } - - tmpl, err := templates.Parse(templateFormat) - if err != nil { - return cli.StatusError{StatusCode: 64, - Status: "Template parsing error: " + err.Error()} - } - - vd := versionInfo{ - Client: clientVersion{ - Version: dockerversion.Version, - APIVersion: dockerCli.Client().ClientVersion(), - DefaultAPIVersion: dockerCli.DefaultVersion(), - GoVersion: runtime.Version(), - GitCommit: dockerversion.GitCommit, - BuildTime: dockerversion.BuildTime, - Os: runtime.GOOS, - Arch: runtime.GOARCH, - }, - } - - serverVersion, err := dockerCli.Client().ServerVersion(ctx) - if err == nil { - vd.Server = &serverVersion - } - - // first we need to make BuildTime more human friendly - t, errTime := time.Parse(time.RFC3339Nano, vd.Client.BuildTime) - if errTime == nil { - vd.Client.BuildTime = t.Format(time.ANSIC) - } - - if vd.ServerOK() { - t, errTime = time.Parse(time.RFC3339Nano, vd.Server.BuildTime) - if errTime == nil { - vd.Server.BuildTime = t.Format(time.ANSIC) - } - } - - if err2 := tmpl.Execute(dockerCli.Out(), vd); err2 != nil && err == nil { - err = err2 - } - dockerCli.Out().Write([]byte{'\n'}) - return err -} diff --git a/cli/command/task/print.go b/cli/command/task/print.go deleted file mode 100644 index 3df3b2985a..0000000000 --- a/cli/command/task/print.go +++ /dev/null @@ -1,84 +0,0 @@ -package task - -import ( - "fmt" - "sort" - - "golang.org/x/net/context" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/cli/command/idresolver" -) - -type tasksBySlot []swarm.Task - -func (t tasksBySlot) Len() int { - return len(t) -} - -func (t tasksBySlot) Swap(i, j int) { - t[i], t[j] = t[j], t[i] -} - -func (t tasksBySlot) Less(i, j int) bool { - // Sort by slot. - if t[i].Slot != t[j].Slot { - return t[i].Slot < t[j].Slot - } - - // If same slot, sort by most recent. - return t[j].Meta.CreatedAt.Before(t[i].CreatedAt) -} - -// Print task information in a format. -// Besides this, command `docker node ps ` -// and `docker stack ps` will call this, too. -func Print(dockerCli command.Cli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, trunc, quiet bool, format string) error { - sort.Stable(tasksBySlot(tasks)) - - names := map[string]string{} - nodes := map[string]string{} - - tasksCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewTaskFormat(format, quiet), - Trunc: trunc, - } - - prevName := "" - for _, task := range tasks { - serviceName, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID) - if err != nil { - return err - } - - nodeValue, err := resolver.Resolve(ctx, swarm.Node{}, task.NodeID) - if err != nil { - return err - } - - name := "" - if task.Slot != 0 { - name = fmt.Sprintf("%v.%v", serviceName, task.Slot) - } else { - name = fmt.Sprintf("%v.%v", serviceName, task.NodeID) - } - - // Indent the name if necessary - indentedName := name - if name == prevName { - indentedName = fmt.Sprintf(" \\_ %s", indentedName) - } - prevName = name - - names[task.ID] = name - if tasksCtx.Format.IsTable() { - names[task.ID] = indentedName - } - nodes[task.ID] = nodeValue - } - - return formatter.TaskWrite(tasksCtx, tasks, names, nodes) -} diff --git a/cli/command/trust.go b/cli/command/trust.go deleted file mode 100644 index c0742bc5b2..0000000000 --- a/cli/command/trust.go +++ /dev/null @@ -1,43 +0,0 @@ -package command - -import ( - "os" - "strconv" - - "github.com/spf13/pflag" -) - -var ( - // TODO: make this not global - untrusted bool -) - -// AddTrustVerificationFlags adds content trust flags to the provided flagset -func AddTrustVerificationFlags(fs *pflag.FlagSet) { - trusted := getDefaultTrustState() - fs.BoolVar(&untrusted, "disable-content-trust", !trusted, "Skip image verification") -} - -// AddTrustSigningFlags adds "signing" flags to the provided flagset -func AddTrustSigningFlags(fs *pflag.FlagSet) { - trusted := getDefaultTrustState() - fs.BoolVar(&untrusted, "disable-content-trust", !trusted, "Skip image signing") -} - -// getDefaultTrustState returns true if content trust is enabled through the $DOCKER_CONTENT_TRUST environment variable. -func getDefaultTrustState() bool { - var trusted bool - if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" { - if t, err := strconv.ParseBool(e); t || err != nil { - // treat any other value as true - trusted = true - } - } - return trusted -} - -// IsTrusted returns true if content trust is enabled, either through the $DOCKER_CONTENT_TRUST environment variable, -// or through `--disabled-content-trust=false` on a command. -func IsTrusted() bool { - return !untrusted -} diff --git a/cli/command/utils.go b/cli/command/utils.go deleted file mode 100644 index 853fe11c78..0000000000 --- a/cli/command/utils.go +++ /dev/null @@ -1,119 +0,0 @@ -package command - -import ( - "bufio" - "fmt" - "io" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/pkg/system" -) - -// CopyToFile writes the content of the reader to the specified file -func CopyToFile(outfile string, r io.Reader) error { - // We use sequential file access here to avoid depleting the standby list - // on Windows. On Linux, this is a call directly to ioutil.TempFile - tmpFile, err := system.TempFileSequential(filepath.Dir(outfile), ".docker_temp_") - if err != nil { - return err - } - - tmpPath := tmpFile.Name() - - _, err = io.Copy(tmpFile, r) - tmpFile.Close() - - if err != nil { - os.Remove(tmpPath) - return err - } - - if err = os.Rename(tmpPath, outfile); err != nil { - os.Remove(tmpPath) - return err - } - - return nil -} - -// capitalizeFirst capitalizes the first character of string -func capitalizeFirst(s string) string { - switch l := len(s); l { - case 0: - return s - case 1: - return strings.ToLower(s) - default: - return strings.ToUpper(string(s[0])) + strings.ToLower(s[1:]) - } -} - -// PrettyPrint outputs arbitrary data for human formatted output by uppercasing the first letter. -func PrettyPrint(i interface{}) string { - switch t := i.(type) { - case nil: - return "None" - case string: - return capitalizeFirst(t) - default: - return capitalizeFirst(fmt.Sprintf("%s", t)) - } -} - -// PromptForConfirmation requests and checks confirmation from user. -// This will display the provided message followed by ' [y/N] '. If -// the user input 'y' or 'Y' it returns true other false. If no -// message is provided "Are you sure you want to proceed? [y/N] " -// will be used instead. -func PromptForConfirmation(ins *InStream, outs *OutStream, message string) bool { - if message == "" { - message = "Are you sure you want to proceed?" - } - message += " [y/N] " - - fmt.Fprintf(outs, message) - - // On Windows, force the use of the regular OS stdin stream. - if runtime.GOOS == "windows" { - ins = NewInStream(os.Stdin) - } - - reader := bufio.NewReader(ins) - answer, _, _ := reader.ReadLine() - return strings.ToLower(string(answer)) == "y" -} - -// PruneFilters returns consolidated prune filters obtained from config.json and cli -func PruneFilters(dockerCli Cli, pruneFilters filters.Args) filters.Args { - if dockerCli.ConfigFile() == nil { - return pruneFilters - } - for _, f := range dockerCli.ConfigFile().PruneFilters { - parts := strings.SplitN(f, "=", 2) - if len(parts) != 2 { - continue - } - if parts[0] == "label" { - // CLI label filter supersede config.json. - // If CLI label filter conflict with config.json, - // skip adding label! filter in config.json. - if pruneFilters.Include("label!") && pruneFilters.ExactMatch("label!", parts[1]) { - continue - } - } else if parts[0] == "label!" { - // CLI label! filter supersede config.json. - // If CLI label! filter conflict with config.json, - // skip adding label filter in config.json. - if pruneFilters.Include("label") && pruneFilters.ExactMatch("label", parts[1]) { - continue - } - } - pruneFilters.Add(parts[0], parts[1]) - } - - return pruneFilters -} diff --git a/cli/command/volume/client_test.go b/cli/command/volume/client_test.go deleted file mode 100644 index c29655cdb0..0000000000 --- a/cli/command/volume/client_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package volume - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - volumetypes "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -type fakeClient struct { - client.Client - volumeCreateFunc func(volumetypes.VolumesCreateBody) (types.Volume, error) - volumeInspectFunc func(volumeID string) (types.Volume, error) - volumeListFunc func(filter filters.Args) (volumetypes.VolumesListOKBody, error) - volumeRemoveFunc func(volumeID string, force bool) error - volumePruneFunc func(filter filters.Args) (types.VolumesPruneReport, error) -} - -func (c *fakeClient) VolumeCreate(ctx context.Context, options volumetypes.VolumesCreateBody) (types.Volume, error) { - if c.volumeCreateFunc != nil { - return c.volumeCreateFunc(options) - } - return types.Volume{}, nil -} - -func (c *fakeClient) VolumeInspect(ctx context.Context, volumeID string) (types.Volume, error) { - if c.volumeInspectFunc != nil { - return c.volumeInspectFunc(volumeID) - } - return types.Volume{}, nil -} - -func (c *fakeClient) VolumeList(ctx context.Context, filter filters.Args) (volumetypes.VolumesListOKBody, error) { - if c.volumeListFunc != nil { - return c.volumeListFunc(filter) - } - return volumetypes.VolumesListOKBody{}, nil -} - -func (c *fakeClient) VolumesPrune(ctx context.Context, filter filters.Args) (types.VolumesPruneReport, error) { - if c.volumePruneFunc != nil { - return c.volumePruneFunc(filter) - } - return types.VolumesPruneReport{}, nil -} - -func (c *fakeClient) VolumeRemove(ctx context.Context, volumeID string, force bool) error { - if c.volumeRemoveFunc != nil { - return c.volumeRemoveFunc(volumeID, force) - } - return nil -} diff --git a/cli/command/volume/cmd.go b/cli/command/volume/cmd.go deleted file mode 100644 index 9086c99248..0000000000 --- a/cli/command/volume/cmd.go +++ /dev/null @@ -1,26 +0,0 @@ -package volume - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -// NewVolumeCommand returns a cobra command for `volume` subcommands -func NewVolumeCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "volume COMMAND", - Short: "Manage volumes", - Args: cli.NoArgs, - RunE: dockerCli.ShowHelp, - Tags: map[string]string{"version": "1.21"}, - } - cmd.AddCommand( - newCreateCommand(dockerCli), - newInspectCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - NewPruneCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/volume/create.go b/cli/command/volume/create.go deleted file mode 100644 index 8392cf0029..0000000000 --- a/cli/command/volume/create.go +++ /dev/null @@ -1,70 +0,0 @@ -package volume - -import ( - "fmt" - - volumetypes "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/pkg/errors" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type createOptions struct { - name string - driver string - driverOpts opts.MapOpts - labels opts.ListOpts -} - -func newCreateCommand(dockerCli command.Cli) *cobra.Command { - opts := createOptions{ - driverOpts: *opts.NewMapOpts(nil, nil), - labels: opts.NewListOpts(opts.ValidateEnv), - } - - cmd := &cobra.Command{ - Use: "create [OPTIONS] [VOLUME]", - Short: "Create a volume", - Args: cli.RequiresMaxArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 1 { - if opts.name != "" { - return errors.Errorf("Conflicting options: either specify --name or provide positional arg, not both\n") - } - opts.name = args[0] - } - return runCreate(dockerCli, opts) - }, - } - flags := cmd.Flags() - flags.StringVarP(&opts.driver, "driver", "d", "local", "Specify volume driver name") - flags.StringVar(&opts.name, "name", "", "Specify volume name") - flags.Lookup("name").Hidden = true - flags.VarP(&opts.driverOpts, "opt", "o", "Set driver specific options") - flags.Var(&opts.labels, "label", "Set metadata for a volume") - - return cmd -} - -func runCreate(dockerCli command.Cli, opts createOptions) error { - client := dockerCli.Client() - - volReq := volumetypes.VolumesCreateBody{ - Driver: opts.driver, - DriverOpts: opts.driverOpts.GetAll(), - Name: opts.name, - Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()), - } - - vol, err := client.VolumeCreate(context.Background(), volReq) - if err != nil { - return err - } - - fmt.Fprintf(dockerCli.Out(), "%s\n", vol.Name) - return nil -} diff --git a/cli/command/volume/create_test.go b/cli/command/volume/create_test.go deleted file mode 100644 index 45cf631134..0000000000 --- a/cli/command/volume/create_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package volume - -import ( - "bytes" - "io/ioutil" - "strings" - "testing" - - "github.com/docker/docker/api/types" - volumetypes "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/pkg/testutil" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestVolumeCreateErrors(t *testing.T) { - testCases := []struct { - args []string - flags map[string]string - volumeCreateFunc func(volumetypes.VolumesCreateBody) (types.Volume, error) - expectedError string - }{ - { - args: []string{"volumeName"}, - flags: map[string]string{ - "name": "volumeName", - }, - expectedError: "Conflicting options: either specify --name or provide positional arg, not both", - }, - { - args: []string{"too", "many"}, - expectedError: "requires at most 1 argument(s)", - }, - { - volumeCreateFunc: func(createBody volumetypes.VolumesCreateBody) (types.Volume, error) { - return types.Volume{}, errors.Errorf("error creating volume") - }, - expectedError: "error creating volume", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newCreateCommand( - test.NewFakeCli(&fakeClient{ - volumeCreateFunc: tc.volumeCreateFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestVolumeCreateWithName(t *testing.T) { - name := "foo" - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - volumeCreateFunc: func(body volumetypes.VolumesCreateBody) (types.Volume, error) { - if body.Name != name { - return types.Volume{}, errors.Errorf("expected name %q, got %q", name, body.Name) - } - return types.Volume{ - Name: body.Name, - }, nil - }, - }, buf) - - // Test by flags - cmd := newCreateCommand(cli) - cmd.Flags().Set("name", name) - assert.NoError(t, cmd.Execute()) - assert.Equal(t, name, strings.TrimSpace(buf.String())) - - // Then by args - buf.Reset() - cmd = newCreateCommand(cli) - cmd.SetArgs([]string{name}) - assert.NoError(t, cmd.Execute()) - assert.Equal(t, name, strings.TrimSpace(buf.String())) -} - -func TestVolumeCreateWithFlags(t *testing.T) { - expectedDriver := "foo" - expectedOpts := map[string]string{ - "bar": "1", - "baz": "baz", - } - expectedLabels := map[string]string{ - "lbl1": "v1", - "lbl2": "v2", - } - name := "banana" - - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - volumeCreateFunc: func(body volumetypes.VolumesCreateBody) (types.Volume, error) { - if body.Name != "" { - return types.Volume{}, errors.Errorf("expected empty name, got %q", body.Name) - } - if body.Driver != expectedDriver { - return types.Volume{}, errors.Errorf("expected driver %q, got %q", expectedDriver, body.Driver) - } - if !compareMap(body.DriverOpts, expectedOpts) { - return types.Volume{}, errors.Errorf("expected drivers opts %v, got %v", expectedOpts, body.DriverOpts) - } - if !compareMap(body.Labels, expectedLabels) { - return types.Volume{}, errors.Errorf("expected labels %v, got %v", expectedLabels, body.Labels) - } - return types.Volume{ - Name: name, - }, nil - }, - }, buf) - - cmd := newCreateCommand(cli) - cmd.Flags().Set("driver", "foo") - cmd.Flags().Set("opt", "bar=1") - cmd.Flags().Set("opt", "baz=baz") - cmd.Flags().Set("label", "lbl1=v1") - cmd.Flags().Set("label", "lbl2=v2") - assert.NoError(t, cmd.Execute()) - assert.Equal(t, name, strings.TrimSpace(buf.String())) -} - -func compareMap(actual map[string]string, expected map[string]string) bool { - if len(actual) != len(expected) { - return false - } - for key, value := range actual { - if expectedValue, ok := expected[key]; ok { - if expectedValue != value { - return false - } - } else { - return false - } - } - return true -} diff --git a/cli/command/volume/inspect.go b/cli/command/volume/inspect.go deleted file mode 100644 index 70db264951..0000000000 --- a/cli/command/volume/inspect.go +++ /dev/null @@ -1,45 +0,0 @@ -package volume - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type inspectOptions struct { - format string - names []string -} - -func newInspectCommand(dockerCli command.Cli) *cobra.Command { - var opts inspectOptions - - cmd := &cobra.Command{ - Use: "inspect [OPTIONS] VOLUME [VOLUME...]", - Short: "Display detailed information on one or more volumes", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.names = args - return runInspect(dockerCli, opts) - }, - } - - cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - - return cmd -} - -func runInspect(dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() - - ctx := context.Background() - - getVolFunc := func(name string) (interface{}, []byte, error) { - i, err := client.VolumeInspect(ctx, name) - return i, nil, err - } - - return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getVolFunc) -} diff --git a/cli/command/volume/inspect_test.go b/cli/command/volume/inspect_test.go deleted file mode 100644 index bc1b526440..0000000000 --- a/cli/command/volume/inspect_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package volume - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestVolumeInspectErrors(t *testing.T) { - testCases := []struct { - args []string - flags map[string]string - volumeInspectFunc func(volumeID string) (types.Volume, error) - expectedError string - }{ - { - expectedError: "requires at least 1 argument", - }, - { - args: []string{"foo"}, - volumeInspectFunc: func(volumeID string) (types.Volume, error) { - return types.Volume{}, errors.Errorf("error while inspecting the volume") - }, - expectedError: "error while inspecting the volume", - }, - { - args: []string{"foo"}, - flags: map[string]string{ - "format": "{{invalid format}}", - }, - expectedError: "Template parsing error", - }, - { - args: []string{"foo", "bar"}, - volumeInspectFunc: func(volumeID string) (types.Volume, error) { - if volumeID == "foo" { - return types.Volume{ - Name: "foo", - }, nil - } - return types.Volume{}, errors.Errorf("error while inspecting the volume") - }, - expectedError: "error while inspecting the volume", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newInspectCommand( - test.NewFakeCli(&fakeClient{ - volumeInspectFunc: tc.volumeInspectFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestVolumeInspectWithoutFormat(t *testing.T) { - testCases := []struct { - name string - args []string - volumeInspectFunc func(volumeID string) (types.Volume, error) - }{ - { - name: "single-volume", - args: []string{"foo"}, - volumeInspectFunc: func(volumeID string) (types.Volume, error) { - if volumeID != "foo" { - return types.Volume{}, errors.Errorf("Invalid volumeID, expected %s, got %s", "foo", volumeID) - } - return *Volume(), nil - }, - }, - { - name: "multiple-volume-with-labels", - args: []string{"foo", "bar"}, - volumeInspectFunc: func(volumeID string) (types.Volume, error) { - return *Volume(VolumeName(volumeID), VolumeLabels(map[string]string{ - "foo": "bar", - })), nil - }, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newInspectCommand( - test.NewFakeCli(&fakeClient{ - volumeInspectFunc: tc.volumeInspectFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("volume-inspect-without-format.%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} - -func TestVolumeInspectWithFormat(t *testing.T) { - volumeInspectFunc := func(volumeID string) (types.Volume, error) { - return *Volume(VolumeLabels(map[string]string{ - "foo": "bar", - })), nil - } - testCases := []struct { - name string - format string - args []string - volumeInspectFunc func(volumeID string) (types.Volume, error) - }{ - { - name: "simple-template", - format: "{{.Name}}", - args: []string{"foo"}, - volumeInspectFunc: volumeInspectFunc, - }, - { - name: "json-template", - format: "{{json .Labels}}", - args: []string{"foo"}, - volumeInspectFunc: volumeInspectFunc, - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newInspectCommand( - test.NewFakeCli(&fakeClient{ - volumeInspectFunc: tc.volumeInspectFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - cmd.Flags().Set("format", tc.format) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), fmt.Sprintf("volume-inspect-with-format.%s.golden", tc.name)) - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) - } -} diff --git a/cli/command/volume/list.go b/cli/command/volume/list.go deleted file mode 100644 index 3577db9554..0000000000 --- a/cli/command/volume/list.go +++ /dev/null @@ -1,73 +0,0 @@ -package volume - -import ( - "sort" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/opts" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type byVolumeName []*types.Volume - -func (r byVolumeName) Len() int { return len(r) } -func (r byVolumeName) Swap(i, j int) { r[i], r[j] = r[j], r[i] } -func (r byVolumeName) Less(i, j int) bool { - return r[i].Name < r[j].Name -} - -type listOptions struct { - quiet bool - format string - filter opts.FilterOpt -} - -func newListCommand(dockerCli command.Cli) *cobra.Command { - opts := listOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ls [OPTIONS]", - Aliases: []string{"list"}, - Short: "List volumes", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display volume names") - flags.StringVar(&opts.format, "format", "", "Pretty-print volumes using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Provide filter values (e.g. 'dangling=true')") - - return cmd -} - -func runList(dockerCli command.Cli, opts listOptions) error { - client := dockerCli.Client() - volumes, err := client.VolumeList(context.Background(), opts.filter.Value()) - if err != nil { - return err - } - - format := opts.format - if len(format) == 0 { - if len(dockerCli.ConfigFile().VolumesFormat) > 0 && !opts.quiet { - format = dockerCli.ConfigFile().VolumesFormat - } else { - format = formatter.TableFormatKey - } - } - - sort.Sort(byVolumeName(volumes.Volumes)) - - volumeCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: formatter.NewVolumeFormat(format, opts.quiet), - } - return formatter.VolumeWrite(volumeCtx, volumes.Volumes) -} diff --git a/cli/command/volume/list_test.go b/cli/command/volume/list_test.go deleted file mode 100644 index 4f5e99389f..0000000000 --- a/cli/command/volume/list_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package volume - -import ( - "bytes" - "io/ioutil" - "testing" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - volumetypes "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/cli/config/configfile" - "github.com/docker/docker/cli/internal/test" - "github.com/pkg/errors" - // Import builders to get the builder function as package function - . "github.com/docker/docker/cli/internal/test/builders" - "github.com/docker/docker/pkg/testutil" - "github.com/docker/docker/pkg/testutil/golden" - "github.com/stretchr/testify/assert" -) - -func TestVolumeListErrors(t *testing.T) { - testCases := []struct { - args []string - flags map[string]string - volumeListFunc func(filter filters.Args) (volumetypes.VolumesListOKBody, error) - expectedError string - }{ - { - args: []string{"foo"}, - expectedError: "accepts no argument", - }, - { - volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { - return volumetypes.VolumesListOKBody{}, errors.Errorf("error listing volumes") - }, - expectedError: "error listing volumes", - }, - } - for _, tc := range testCases { - buf := new(bytes.Buffer) - cmd := newListCommand( - test.NewFakeCli(&fakeClient{ - volumeListFunc: tc.volumeListFunc, - }, buf), - ) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOutput(ioutil.Discard) - testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestVolumeListWithoutFormat(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { - return volumetypes.VolumesListOKBody{ - Volumes: []*types.Volume{ - Volume(), - Volume(VolumeName("foo"), VolumeDriver("bar")), - Volume(VolumeName("baz"), VolumeLabels(map[string]string{ - "foo": "bar", - })), - }, - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newListCommand(cli) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "volume-list-without-format.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) -} - -func TestVolumeListWithConfigFormat(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { - return volumetypes.VolumesListOKBody{ - Volumes: []*types.Volume{ - Volume(), - Volume(VolumeName("foo"), VolumeDriver("bar")), - Volume(VolumeName("baz"), VolumeLabels(map[string]string{ - "foo": "bar", - })), - }, - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{ - VolumesFormat: "{{ .Name }} {{ .Driver }} {{ .Labels }}", - }) - cmd := newListCommand(cli) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "volume-list-with-config-format.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) -} - -func TestVolumeListWithFormat(t *testing.T) { - buf := new(bytes.Buffer) - cli := test.NewFakeCli(&fakeClient{ - volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { - return volumetypes.VolumesListOKBody{ - Volumes: []*types.Volume{ - Volume(), - Volume(VolumeName("foo"), VolumeDriver("bar")), - Volume(VolumeName("baz"), VolumeLabels(map[string]string{ - "foo": "bar", - })), - }, - }, nil - }, - }, buf) - cli.SetConfigfile(&configfile.ConfigFile{}) - cmd := newListCommand(cli) - cmd.Flags().Set("format", "{{ .Name }} {{ .Driver }} {{ .Labels }}") - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "volume-list-with-format.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) -} diff --git a/cli/command/volume/prune.go b/cli/command/volume/prune.go deleted file mode 100644 index f7d823ffac..0000000000 --- a/cli/command/volume/prune.go +++ /dev/null @@ -1,78 +0,0 @@ -package volume - -import ( - "fmt" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/opts" - units "github.com/docker/go-units" - "github.com/spf13/cobra" - "golang.org/x/net/context" -) - -type pruneOptions struct { - force bool - filter opts.FilterOpt -} - -// NewPruneCommand returns a new cobra prune command for volumes -func NewPruneCommand(dockerCli command.Cli) *cobra.Command { - opts := pruneOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "prune [OPTIONS]", - Short: "Remove all unused volumes", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - spaceReclaimed, output, err := runPrune(dockerCli, opts) - if err != nil { - return err - } - if output != "" { - fmt.Fprintln(dockerCli.Out(), output) - } - fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) - return nil - }, - Tags: map[string]string{"version": "1.25"}, - } - - flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") - flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'label=