Merge component 'cli' from git@github.com:docker/cli master
This commit is contained in:
4
components/cli/.gitignore
vendored
4
components/cli/.gitignore
vendored
@ -2,5 +2,9 @@
|
||||
/build/
|
||||
cli/winresources/rsrc_386.syso
|
||||
cli/winresources/rsrc_amd64.syso
|
||||
/man/man1/
|
||||
/man/man5/
|
||||
/man/man8/
|
||||
/docs/yaml/gen/
|
||||
coverage.txt
|
||||
profile.out
|
||||
|
||||
@ -6,17 +6,17 @@ all: binary
|
||||
# remove build artifacts
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf ./build/* cli/winresources/rsrc_*
|
||||
rm -rf ./build/* cli/winresources/rsrc_* ./man/man[1-9] docs/yaml/gen
|
||||
|
||||
# run go test
|
||||
# the "-tags daemon" part is temporary
|
||||
.PHONY: test
|
||||
test:
|
||||
./scripts/test/unit $(shell go list ./... | grep -v /vendor/)
|
||||
./scripts/test/unit $(shell go list ./... | grep -v '/vendor/')
|
||||
|
||||
.PHONY: test-coverage
|
||||
test-coverage:
|
||||
./scripts/test/unit-with-coverage
|
||||
./scripts/test/unit-with-coverage $(shell go list ./... | grep -v '/vendor/')
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@ -44,6 +44,16 @@ vendor: vendor.conf
|
||||
vndr 2> /dev/null
|
||||
scripts/validate/check-git-diff vendor
|
||||
|
||||
## generate man pages from go source and markdown
|
||||
.PHONY: manpages
|
||||
manpages:
|
||||
scripts/docs/generate-man.sh
|
||||
|
||||
## generate documentation YAML files consumed by docs repo
|
||||
.PHONY: yamldocs
|
||||
yamldocs:
|
||||
scripts/docs/generate-yaml.sh
|
||||
|
||||
cli/compose/schema/bindata.go: cli/compose/schema/data/*.json
|
||||
go generate github.com/docker/cli/cli/compose/schema
|
||||
|
||||
|
||||
@ -1 +1 @@
|
||||
17.06.0-dev
|
||||
17.07.0-dev
|
||||
|
||||
@ -1,52 +1,81 @@
|
||||
version: 2
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
lint:
|
||||
working_directory: /work
|
||||
docker:
|
||||
- image: docker:17.05
|
||||
parallelism: 4
|
||||
docker: [{image: 'docker:17.05-git'}]
|
||||
steps:
|
||||
- run:
|
||||
name: "Install Git and SSH"
|
||||
command: apk add -U git openssh
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- run:
|
||||
command: docker version
|
||||
- run:
|
||||
name: "Lint"
|
||||
command: |
|
||||
if [ "$CIRCLE_NODE_INDEX" != "0" ]; then exit; fi
|
||||
dockerfile=dockerfiles/Dockerfile.lint
|
||||
echo "COPY . ." >> $dockerfile
|
||||
docker build -f $dockerfile --tag cli-linter .
|
||||
docker run cli-linter
|
||||
|
||||
cross:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:17.05-git'}]
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- run:
|
||||
name: "Cross"
|
||||
command: |
|
||||
if [ "$CIRCLE_NODE_INDEX" != "1" ]; then exit; fi
|
||||
dockerfile=dockerfiles/Dockerfile.cross
|
||||
echo "COPY . ." >> $dockerfile
|
||||
docker build -f $dockerfile --tag cli-builder .
|
||||
docker run --name cross cli-builder make cross
|
||||
docker cp cross:/go/src/github.com/docker/cli/build /work/build
|
||||
- store_artifacts:
|
||||
path: /work/build
|
||||
|
||||
test:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:17.05-git'}]
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- run:
|
||||
name: "Unit Test with Coverage"
|
||||
command: |
|
||||
if [ "$CIRCLE_NODE_INDEX" != "2" ]; then exit; fi
|
||||
dockerfile=dockerfiles/Dockerfile.dev
|
||||
echo "COPY . ." >> $dockerfile
|
||||
docker build -f $dockerfile --tag cli-builder .
|
||||
docker run --name test cli-builder make test-coverage
|
||||
|
||||
- run:
|
||||
name: "Upload to Codecov"
|
||||
command: |
|
||||
docker cp test:/go/src/github.com/docker/cli/coverage.txt coverage.txt
|
||||
apk add -U bash curl
|
||||
curl -s https://codecov.io/bash | bash
|
||||
|
||||
validate:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:17.05-git'}]
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- run:
|
||||
name: "Validate Vendor and Code Generation"
|
||||
name: "Validate Vendor, Docs, and Code Generation"
|
||||
command: |
|
||||
if [ "$CIRCLE_NODE_INDEX" != "3" ]; then exit; fi
|
||||
dockerfile=dockerfiles/Dockerfile.dev
|
||||
echo "COPY . ." >> $dockerfile
|
||||
rm -f .dockerignore # include .git
|
||||
docker build -f $dockerfile --tag cli-builder .
|
||||
docker run cli-builder make -B vendor compose-jsonschema
|
||||
docker run cli-builder make -B vendor compose-jsonschema manpages yamldocs
|
||||
|
||||
- store_artifacts:
|
||||
path: /work/build
|
||||
workflows:
|
||||
version: 2
|
||||
ci:
|
||||
jobs:
|
||||
- lint
|
||||
- cross
|
||||
- test
|
||||
- validate
|
||||
|
||||
@ -202,6 +202,9 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
|
||||
if versions.LessThan(ping.APIVersion, cli.client.ClientVersion()) {
|
||||
cli.client.UpdateClientVersion(ping.APIVersion)
|
||||
}
|
||||
} else {
|
||||
// Default to true if we fail to connect to daemon
|
||||
cli.server = ServerInfo{HasExperimental: true}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@ -10,7 +10,6 @@ import (
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"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"
|
||||
@ -65,7 +64,7 @@ func runConfigCreate(dockerCli command.Cli, options createOptions) error {
|
||||
spec := swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: options.name,
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
},
|
||||
Data: configData,
|
||||
}
|
||||
|
||||
@ -77,6 +77,7 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
|
||||
|
||||
cmd := newConfigRemoveCommand(cli)
|
||||
cmd.SetArgs(names)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
assert.EqualError(t, cmd.Execute(), "error removing config: foo")
|
||||
assert.Equal(t, names, removedConfigs)
|
||||
}
|
||||
|
||||
@ -34,11 +34,15 @@ func inspectContainerAndCheckState(ctx context.Context, cli client.APIClient, ar
|
||||
if c.State.Paused {
|
||||
return nil, errors.New("You cannot attach to a paused container, unpause it first")
|
||||
}
|
||||
if c.State.Restarting {
|
||||
return nil, errors.New("You cannot attach to a restarting container, wait until it is running")
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// NewAttachCommand creates a new cobra.Command for `docker attach`
|
||||
func NewAttachCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
func NewAttachCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts attachOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -58,7 +62,7 @@ func NewAttachCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runAttach(dockerCli *command.DockerCli, opts *attachOptions) error {
|
||||
func runAttach(dockerCli command.Cli, opts *attachOptions) error {
|
||||
ctx := context.Background()
|
||||
client := dockerCli.Client()
|
||||
|
||||
|
||||
77
components/cli/cli/command/container/attach_test.go
Normal file
77
components/cli/cli/command/container/attach_test.go
Normal file
@ -0,0 +1,77 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestNewAttachCommandErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
containerInspectFunc func(img string) (types.ContainerJSON, error)
|
||||
}{
|
||||
{
|
||||
name: "client-error",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "something went wrong",
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
return types.ContainerJSON{}, errors.Errorf("something went wrong")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client-stopped",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "You cannot attach to a stopped container",
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
c := types.ContainerJSON{}
|
||||
c.ContainerJSONBase = &types.ContainerJSONBase{}
|
||||
c.ContainerJSONBase.State = &types.ContainerState{Running: false}
|
||||
return c, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client-paused",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "You cannot attach to a paused container",
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
c := types.ContainerJSON{}
|
||||
c.ContainerJSONBase = &types.ContainerJSONBase{}
|
||||
c.ContainerJSONBase.State = &types.ContainerState{
|
||||
Running: true,
|
||||
Paused: true,
|
||||
}
|
||||
return c, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client-restarting",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "You cannot attach to a restarting container",
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
c := types.ContainerJSON{}
|
||||
c.ContainerJSONBase = &types.ContainerJSONBase{}
|
||||
c.ContainerJSONBase.State = &types.ContainerState{
|
||||
Running: true,
|
||||
Paused: false,
|
||||
Restarting: true,
|
||||
}
|
||||
return c, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{containerInspectFunc: tc.containerInspectFunc}, buf))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
19
components/cli/cli/command/container/client_test.go
Normal file
19
components/cli/cli/command/container/client_test.go
Normal file
@ -0,0 +1,19 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
containerInspectFunc func(string) (types.ContainerJSON, error)
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
|
||||
if cli.containerInspectFunc != nil {
|
||||
return cli.containerInspectFunc(containerID)
|
||||
}
|
||||
return types.ContainerJSON{}, nil
|
||||
}
|
||||
@ -33,7 +33,7 @@ func newExecOptions() *execOptions {
|
||||
}
|
||||
|
||||
// NewExecCommand creates a new cobra.Command for `docker exec`
|
||||
func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options := newExecOptions()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -63,7 +63,7 @@ func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
func runExec(dockerCli *command.DockerCli, options *execOptions, container string, execCmd []string) error {
|
||||
func runExec(dockerCli command.Cli, options *execOptions, container string, execCmd []string) error {
|
||||
execConfig, err := parseExec(options, execCmd)
|
||||
// just in case the ParseExec does not exit
|
||||
if container == "" || err != nil {
|
||||
@ -111,12 +111,7 @@ func runExec(dockerCli *command.DockerCli, options *execOptions, container strin
|
||||
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
|
||||
return client.ContainerExecStart(ctx, execID, execStartCheck)
|
||||
}
|
||||
|
||||
// Interactive exec requested.
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type arguments struct {
|
||||
@ -114,3 +120,31 @@ func compareExecConfig(config1 *types.ExecConfig, config2 *types.ExecConfig) boo
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestNewExecCommandErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
containerInspectFunc func(img string) (types.ContainerJSON, error)
|
||||
}{
|
||||
{
|
||||
name: "client-error",
|
||||
args: []string{"5cb5bb5e4a3b", "-t", "-i", "bash"},
|
||||
expectedError: "something went wrong",
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
return types.ContainerJSON{}, errors.Errorf("something went wrong")
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
conf := configfile.ConfigFile{}
|
||||
cli := test.NewFakeCli(&fakeClient{containerInspectFunc: tc.containerInspectFunc}, buf)
|
||||
cli.SetConfigfile(&conf)
|
||||
cmd := NewExecCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,7 +110,7 @@ func (h *hijackedIOStreamer) setupInput() (restore func(), err error) {
|
||||
|
||||
func (h *hijackedIOStreamer) beginOutputStream(restoreInput func()) <-chan error {
|
||||
if h.outputStream == nil && h.errorStream == nil {
|
||||
// Ther is no need to copy output.
|
||||
// There is no need to copy output.
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -163,7 +163,7 @@ func (h *hijackedIOStreamer) beginInputStream(restoreInput func()) (doneC <-chan
|
||||
if err != nil {
|
||||
// This error will also occur on the receive
|
||||
// side (from stdout) where it will be
|
||||
// propogated back to the caller.
|
||||
// propagated back to the caller.
|
||||
logrus.Debugf("Error sendStdin: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,19 +12,19 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"github.com/docker/cli/opts"
|
||||
"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/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}$")
|
||||
deviceCgroupRuleRegexp = regexp.MustCompile(`^[acb] ([0-9]+|\*):([0-9]+|\*) [rwm]{1,3}$`)
|
||||
)
|
||||
|
||||
// containerOptions is a data object with all the options for creating a container
|
||||
@ -333,7 +333,8 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
|
||||
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 {
|
||||
parsed, _ := loader.ParseVolume(bind)
|
||||
if parsed.Source != "" {
|
||||
// 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)
|
||||
@ -409,13 +410,13 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
|
||||
}
|
||||
|
||||
// collect all the environment variables for the container
|
||||
envVariables, err := runconfigopts.ReadKVStrings(copts.envFile.GetAll(), copts.env.GetAll())
|
||||
envVariables, err := opts.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())
|
||||
labels, err := opts.ReadKVStrings(copts.labelsFile.GetAll(), copts.labels.GetAll())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -440,7 +441,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
|
||||
return nil, errors.Errorf("--userns: invalid USER mode")
|
||||
}
|
||||
|
||||
restartPolicy, err := runconfigopts.ParseRestartPolicy(copts.restartPolicy)
|
||||
restartPolicy, err := opts.ParseRestartPolicy(copts.restartPolicy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -553,7 +554,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
|
||||
MacAddress: copts.macAddress,
|
||||
Entrypoint: entrypoint,
|
||||
WorkingDir: copts.workingDir,
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(labels),
|
||||
Labels: opts.ConvertKVStringsToMap(labels),
|
||||
Healthcheck: healthConfig,
|
||||
}
|
||||
if flags.Changed("stop-signal") {
|
||||
@ -666,7 +667,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
|
||||
}
|
||||
|
||||
func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) {
|
||||
loggingOptsMap := runconfigopts.ConvertKVStringsToMap(loggingOpts)
|
||||
loggingOptsMap := opts.ConvertKVStringsToMap(loggingOpts)
|
||||
if loggingDriver == "none" && len(loggingOpts) > 0 {
|
||||
return map[string]string{}, errors.Errorf("invalid logging opts for driver %s", loggingDriver)
|
||||
}
|
||||
@ -828,67 +829,6 @@ func validatePath(val string, validator func(string) bool) (string, error) {
|
||||
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)
|
||||
|
||||
@ -19,6 +19,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateAttach(t *testing.T) {
|
||||
@ -61,16 +62,14 @@ func parseRun(args []string) (*container.Config, *container.HostConfig, *network
|
||||
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 parseMustError(t *testing.T, args string) {
|
||||
_, _, _, err := parseRun(strings.Split(args+" ubuntu bash", " "))
|
||||
assert.Error(t, err, args)
|
||||
}
|
||||
|
||||
func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig) {
|
||||
config, hostConfig, err := parsetest(t, args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config, hostConfig, _, err := parseRun(append(strings.Split(args, " "), "ubuntu", "bash"))
|
||||
assert.NoError(t, err)
|
||||
return config, hostConfig
|
||||
}
|
||||
|
||||
@ -86,7 +85,6 @@ func TestParseRunLinks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
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)
|
||||
@ -103,31 +101,17 @@ func TestParseRunAttach(t *testing.T) {
|
||||
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 TestParseRunWithInvalidArgs(t *testing.T) {
|
||||
parseMustError(t, "-a")
|
||||
parseMustError(t, "-a invalid")
|
||||
parseMustError(t, "-a invalid -a stdout")
|
||||
parseMustError(t, "-a stdout -a stderr -d")
|
||||
parseMustError(t, "-a stdin -d")
|
||||
parseMustError(t, "-a stdout -d")
|
||||
parseMustError(t, "-a stderr -d")
|
||||
parseMustError(t, "-d --rm")
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
@ -383,51 +367,46 @@ func TestParseDevice(t *testing.T) {
|
||||
|
||||
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)
|
||||
}
|
||||
_, _, _, err := parseRun([]string{"--ipc=container:", "img", "cmd"})
|
||||
testutil.ErrorContains(t, err, "--ipc: invalid IPC mode")
|
||||
|
||||
// ipc ok
|
||||
_, hostconfig, _, err := parseRun([]string{"--ipc=host", "img", "cmd"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, 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)
|
||||
}
|
||||
_, _, _, err = parseRun([]string{"--pid=container:", "img", "cmd"})
|
||||
testutil.ErrorContains(t, err, "--pid: invalid PID mode")
|
||||
|
||||
// pid ok
|
||||
_, hostconfig, _, err = parseRun([]string{"--pid=host", "img", "cmd"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, 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)
|
||||
}
|
||||
_, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"})
|
||||
testutil.ErrorContains(t, err, "--uts: invalid UTS mode")
|
||||
|
||||
// uts ok
|
||||
_, hostconfig, _, err = parseRun([]string{"--uts=host", "img", "cmd"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, 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)
|
||||
}
|
||||
_, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"})
|
||||
testutil.ErrorContains(t, err, expectedErr)
|
||||
|
||||
// shm-size ok
|
||||
_, hostconfig, _, err = parseRun([]string{"--shm-size=128m", "img", "cmd"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
if hostconfig.ShmSize != 134217728 {
|
||||
t.Fatalf("Expected a valid ShmSize, got %d", hostconfig.ShmSize)
|
||||
}
|
||||
@ -771,58 +750,6 @@ func callDecodeContainerConfig(volumes []string, binds []string) (*container.Con
|
||||
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",
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
@ -12,12 +13,12 @@ import (
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/promise"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
"github.com/docker/docker/pkg/term"
|
||||
"github.com/docker/libnetwork/resolvconf/dns"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@ -77,21 +78,43 @@ func warnOnOomKillDisable(hostConfig container.HostConfig, stderr io.Writer) {
|
||||
// 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) {
|
||||
if 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 {
|
||||
// IPLocalhost is a regex pattern for IPv4 or IPv6 loopback range.
|
||||
const ipLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)`
|
||||
|
||||
var localhostIPRegexp = regexp.MustCompile(ipLocalhost)
|
||||
|
||||
// IsLocalhost returns true if ip matches the localhost IP regular expression.
|
||||
// Used for determining if nameserver settings are being passed which are
|
||||
// localhost addresses
|
||||
func isLocalhost(ip string) bool {
|
||||
return localhostIPRegexp.MatchString(ip)
|
||||
}
|
||||
|
||||
func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, ropts *runOptions, copts *containerOptions) error {
|
||||
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), copts.env.GetAll())
|
||||
newEnv := []string{}
|
||||
for k, v := range proxyConfig {
|
||||
if v == nil {
|
||||
newEnv = append(newEnv, k)
|
||||
} else {
|
||||
newEnv = append(newEnv, fmt.Sprintf("%s=%s", k, *v))
|
||||
}
|
||||
}
|
||||
copts.env = *opts.NewListOptsRef(&newEnv, nil)
|
||||
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)
|
||||
return runContainer(dockerCli, ropts, copts, containerConfig)
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
@ -147,6 +170,7 @@ func runContainer(dockerCli *command.DockerCli, opts *runOptions, copts *contain
|
||||
sigc := ForwardAllSignals(ctx, dockerCli, createResponse.ID)
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
var (
|
||||
waitDisplayID chan struct{}
|
||||
errCh chan error
|
||||
|
||||
@ -146,12 +146,12 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error {
|
||||
fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err)
|
||||
}
|
||||
}
|
||||
if attchErr := <-cErr; attchErr != nil {
|
||||
if attachErr := <-cErr; attachErr != nil {
|
||||
if _, ok := err.(term.EscapeError); ok {
|
||||
// The user entered the detach escape sequence.
|
||||
return nil
|
||||
}
|
||||
return attchErr
|
||||
return attachErr
|
||||
}
|
||||
|
||||
if status := <-statusChan; status != 0 {
|
||||
|
||||
@ -106,7 +106,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
|
||||
closeChan <- err
|
||||
}
|
||||
for _, container := range cs {
|
||||
s := formatter.NewContainerStats(container.ID[:12], daemonOSType)
|
||||
s := formatter.NewContainerStats(container.ID[:12])
|
||||
if cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
|
||||
@ -123,7 +123,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
|
||||
eh := command.InitEventHandler()
|
||||
eh.Handle("create", func(e events.Message) {
|
||||
if opts.all {
|
||||
s := formatter.NewContainerStats(e.ID[:12], daemonOSType)
|
||||
s := formatter.NewContainerStats(e.ID[:12])
|
||||
if cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
|
||||
@ -132,7 +132,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
|
||||
})
|
||||
|
||||
eh.Handle("start", func(e events.Message) {
|
||||
s := formatter.NewContainerStats(e.ID[:12], daemonOSType)
|
||||
s := formatter.NewContainerStats(e.ID[:12])
|
||||
if cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
|
||||
@ -158,7 +158,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
|
||||
// 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)
|
||||
s := formatter.NewContainerStats(name)
|
||||
if cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
|
||||
|
||||
@ -16,9 +16,8 @@ import (
|
||||
)
|
||||
|
||||
type stats struct {
|
||||
ostype string
|
||||
mu sync.Mutex
|
||||
cs []*formatter.ContainerStats
|
||||
mu sync.Mutex
|
||||
cs []*formatter.ContainerStats
|
||||
}
|
||||
|
||||
// daemonOSType is set once we have at least one stat for a container
|
||||
|
||||
@ -39,7 +39,7 @@ func resizeTtyTo(ctx context.Context, client client.ContainerAPIClient, id strin
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func MonitorTtySize(ctx context.Context, cli command.Cli, id string, isExec bool) error {
|
||||
resizeTty := func() {
|
||||
height, width := cli.Out().GetTtySize()
|
||||
resizeTtyTo(ctx, cli.Client(), id, height, width, isExec)
|
||||
@ -74,7 +74,7 @@ func MonitorTtySize(ctx context.Context, cli *command.DockerCli, id string, isEx
|
||||
}
|
||||
|
||||
// ForwardAllSignals forwards signals to the container
|
||||
func ForwardAllSignals(ctx context.Context, cli *command.DockerCli, cid string) chan os.Signal {
|
||||
func ForwardAllSignals(ctx context.Context, cli command.Cli, cid string) chan os.Signal {
|
||||
sigc := make(chan os.Signal, 128)
|
||||
signal.CatchAll(sigc)
|
||||
go func() {
|
||||
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
@ -82,7 +81,7 @@ func runUpdate(dockerCli *command.DockerCli, options *updateOptions) error {
|
||||
|
||||
var restartPolicy containertypes.RestartPolicy
|
||||
if options.restartPolicy != "" {
|
||||
restartPolicy, err = runconfigopts.ParseRestartPolicy(options.restartPolicy)
|
||||
restartPolicy, err = opts.ParseRestartPolicy(options.restartPolicy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -127,7 +127,7 @@ func legacyWaitExitOrRemoved(ctx context.Context, dockerCli *command.DockerCli,
|
||||
|
||||
// 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) {
|
||||
func getExitCode(ctx context.Context, dockerCli command.Cli, 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.
|
||||
|
||||
@ -2,16 +2,16 @@ package formatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"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"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -171,16 +171,16 @@ func (c *containerContext) Command() string {
|
||||
}
|
||||
|
||||
func (c *containerContext) CreatedAt() string {
|
||||
return time.Unix(int64(c.c.Created), 0).String()
|
||||
return time.Unix(c.c.Created, 0).String()
|
||||
}
|
||||
|
||||
func (c *containerContext) RunningFor() string {
|
||||
createdAt := time.Unix(int64(c.c.Created), 0)
|
||||
createdAt := time.Unix(c.c.Created, 0)
|
||||
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
||||
}
|
||||
|
||||
func (c *containerContext) Ports() string {
|
||||
return api.DisplayablePorts(c.c.Ports)
|
||||
return DisplayablePorts(c.c.Ports)
|
||||
}
|
||||
|
||||
func (c *containerContext) Status() string {
|
||||
@ -257,3 +257,89 @@ func (c *containerContext) Networks() string {
|
||||
|
||||
return strings.Join(networks, ",")
|
||||
}
|
||||
|
||||
// DisplayablePorts returns formatted string representing open ports of container
|
||||
// e.g. "0.0.0.0:80->9090/tcp, 9988/tcp"
|
||||
// it's used by command 'docker ps'
|
||||
func DisplayablePorts(ports []types.Port) string {
|
||||
type portGroup struct {
|
||||
first uint16
|
||||
last uint16
|
||||
}
|
||||
groupMap := make(map[string]*portGroup)
|
||||
var result []string
|
||||
var hostMappings []string
|
||||
var groupMapKeys []string
|
||||
sort.Sort(byPortInfo(ports))
|
||||
for _, port := range ports {
|
||||
current := port.PrivatePort
|
||||
portKey := port.Type
|
||||
if port.IP != "" {
|
||||
if port.PublicPort != current {
|
||||
hostMappings = append(hostMappings, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type))
|
||||
continue
|
||||
}
|
||||
portKey = fmt.Sprintf("%s/%s", port.IP, port.Type)
|
||||
}
|
||||
group := groupMap[portKey]
|
||||
|
||||
if group == nil {
|
||||
groupMap[portKey] = &portGroup{first: current, last: current}
|
||||
// record order that groupMap keys are created
|
||||
groupMapKeys = append(groupMapKeys, portKey)
|
||||
continue
|
||||
}
|
||||
if current == (group.last + 1) {
|
||||
group.last = current
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, formGroup(portKey, group.first, group.last))
|
||||
groupMap[portKey] = &portGroup{first: current, last: current}
|
||||
}
|
||||
for _, portKey := range groupMapKeys {
|
||||
g := groupMap[portKey]
|
||||
result = append(result, formGroup(portKey, g.first, g.last))
|
||||
}
|
||||
result = append(result, hostMappings...)
|
||||
return strings.Join(result, ", ")
|
||||
}
|
||||
|
||||
func formGroup(key string, start, last uint16) string {
|
||||
parts := strings.Split(key, "/")
|
||||
groupType := parts[0]
|
||||
var ip string
|
||||
if len(parts) > 1 {
|
||||
ip = parts[0]
|
||||
groupType = parts[1]
|
||||
}
|
||||
group := strconv.Itoa(int(start))
|
||||
if start != last {
|
||||
group = fmt.Sprintf("%s-%d", group, last)
|
||||
}
|
||||
if ip != "" {
|
||||
group = fmt.Sprintf("%s:%s->%s", ip, group, group)
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", group, groupType)
|
||||
}
|
||||
|
||||
// byPortInfo is a temporary type used to sort types.Port by its fields
|
||||
type byPortInfo []types.Port
|
||||
|
||||
func (r byPortInfo) Len() int { return len(r) }
|
||||
func (r byPortInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
||||
func (r byPortInfo) Less(i, j int) bool {
|
||||
if r[i].PrivatePort != r[j].PrivatePort {
|
||||
return r[i].PrivatePort < r[j].PrivatePort
|
||||
}
|
||||
|
||||
if r[i].IP != r[j].IP {
|
||||
return r[i].IP < r[j].IP
|
||||
}
|
||||
|
||||
if r[i].PublicPort != r[j].PublicPort {
|
||||
return r[i].PublicPort < r[j].PublicPort
|
||||
}
|
||||
|
||||
return r[i].Type < r[j].Type
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestContainerPsContext(t *testing.T) {
|
||||
@ -226,7 +227,7 @@ size: 0B
|
||||
Context{Format: NewContainerFormat("{{.Image}}", false, true)},
|
||||
"ubuntu\nubuntu\n",
|
||||
},
|
||||
// Special headers for customerized table format
|
||||
// Special headers for customized 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
|
||||
@ -357,12 +358,11 @@ func TestContainerContextWriteJSON(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
msg := fmt.Sprintf("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)
|
||||
err := json.Unmarshal([]byte(line), &m)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, expectedJSONs[i], m, msg)
|
||||
}
|
||||
}
|
||||
|
||||
@ -377,12 +377,11 @@ func TestContainerContextWriteJSONField(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
msg := fmt.Sprintf("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)
|
||||
err := json.Unmarshal([]byte(line), &s)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, containers[i].ID, s, msg)
|
||||
}
|
||||
}
|
||||
|
||||
@ -411,3 +410,248 @@ func TestContainerBackCompat(t *testing.T) {
|
||||
buf.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
type ports struct {
|
||||
ports []types.Port
|
||||
expected string
|
||||
}
|
||||
|
||||
// nolint: lll
|
||||
func TestDisplayablePorts(t *testing.T) {
|
||||
cases := []ports{
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
PrivatePort: 9988,
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
"9988/tcp"},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
PrivatePort: 9988,
|
||||
Type: "udp",
|
||||
},
|
||||
},
|
||||
"9988/udp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
IP: "0.0.0.0",
|
||||
PrivatePort: 9988,
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
"0.0.0.0:0->9988/tcp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
PrivatePort: 9988,
|
||||
PublicPort: 8899,
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
"9988/tcp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
IP: "4.3.2.1",
|
||||
PrivatePort: 9988,
|
||||
PublicPort: 8899,
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
"4.3.2.1:8899->9988/tcp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
IP: "4.3.2.1",
|
||||
PrivatePort: 9988,
|
||||
PublicPort: 9988,
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
"4.3.2.1:9988->9988/tcp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
PrivatePort: 9988,
|
||||
Type: "udp",
|
||||
}, {
|
||||
PrivatePort: 9988,
|
||||
Type: "udp",
|
||||
},
|
||||
},
|
||||
"9988/udp, 9988/udp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
IP: "1.2.3.4",
|
||||
PublicPort: 9998,
|
||||
PrivatePort: 9998,
|
||||
Type: "udp",
|
||||
}, {
|
||||
IP: "1.2.3.4",
|
||||
PublicPort: 9999,
|
||||
PrivatePort: 9999,
|
||||
Type: "udp",
|
||||
},
|
||||
},
|
||||
"1.2.3.4:9998-9999->9998-9999/udp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
IP: "1.2.3.4",
|
||||
PublicPort: 8887,
|
||||
PrivatePort: 9998,
|
||||
Type: "udp",
|
||||
}, {
|
||||
IP: "1.2.3.4",
|
||||
PublicPort: 8888,
|
||||
PrivatePort: 9999,
|
||||
Type: "udp",
|
||||
},
|
||||
},
|
||||
"1.2.3.4:8887->9998/udp, 1.2.3.4:8888->9999/udp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
PrivatePort: 9998,
|
||||
Type: "udp",
|
||||
}, {
|
||||
PrivatePort: 9999,
|
||||
Type: "udp",
|
||||
},
|
||||
},
|
||||
"9998-9999/udp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
IP: "1.2.3.4",
|
||||
PrivatePort: 6677,
|
||||
PublicPort: 7766,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
PrivatePort: 9988,
|
||||
PublicPort: 8899,
|
||||
Type: "udp",
|
||||
},
|
||||
},
|
||||
"9988/udp, 1.2.3.4:7766->6677/tcp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
IP: "1.2.3.4",
|
||||
PrivatePort: 9988,
|
||||
PublicPort: 8899,
|
||||
Type: "udp",
|
||||
}, {
|
||||
IP: "1.2.3.4",
|
||||
PrivatePort: 9988,
|
||||
PublicPort: 8899,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
IP: "4.3.2.1",
|
||||
PrivatePort: 2233,
|
||||
PublicPort: 3322,
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
"4.3.2.1:3322->2233/tcp, 1.2.3.4:8899->9988/tcp, 1.2.3.4:8899->9988/udp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
PrivatePort: 9988,
|
||||
PublicPort: 8899,
|
||||
Type: "udp",
|
||||
}, {
|
||||
IP: "1.2.3.4",
|
||||
PrivatePort: 6677,
|
||||
PublicPort: 7766,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
IP: "4.3.2.1",
|
||||
PrivatePort: 2233,
|
||||
PublicPort: 3322,
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
"9988/udp, 4.3.2.1:3322->2233/tcp, 1.2.3.4:7766->6677/tcp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
PrivatePort: 80,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
PrivatePort: 1024,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
PrivatePort: 80,
|
||||
Type: "udp",
|
||||
}, {
|
||||
PrivatePort: 1024,
|
||||
Type: "udp",
|
||||
}, {
|
||||
IP: "1.1.1.1",
|
||||
PublicPort: 80,
|
||||
PrivatePort: 1024,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
IP: "1.1.1.1",
|
||||
PublicPort: 80,
|
||||
PrivatePort: 1024,
|
||||
Type: "udp",
|
||||
}, {
|
||||
IP: "1.1.1.1",
|
||||
PublicPort: 1024,
|
||||
PrivatePort: 80,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
IP: "1.1.1.1",
|
||||
PublicPort: 1024,
|
||||
PrivatePort: 80,
|
||||
Type: "udp",
|
||||
}, {
|
||||
IP: "2.1.1.1",
|
||||
PublicPort: 80,
|
||||
PrivatePort: 1024,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
IP: "2.1.1.1",
|
||||
PublicPort: 80,
|
||||
PrivatePort: 1024,
|
||||
Type: "udp",
|
||||
}, {
|
||||
IP: "2.1.1.1",
|
||||
PublicPort: 1024,
|
||||
PrivatePort: 80,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
IP: "2.1.1.1",
|
||||
PublicPort: 1024,
|
||||
PrivatePort: 80,
|
||||
Type: "udp",
|
||||
},
|
||||
},
|
||||
"80/tcp, 80/udp, 1024/tcp, 1024/udp, 1.1.1.1:1024->80/tcp, 1.1.1.1:1024->80/udp, 2.1.1.1:1024->80/tcp, 2.1.1.1:1024->80/udp, 1.1.1.1:80->1024/tcp, 1.1.1.1:80->1024/udp, 2.1.1.1:80->1024/tcp, 2.1.1.1:80->1024/udp",
|
||||
},
|
||||
}
|
||||
|
||||
for _, port := range cases {
|
||||
actual := DisplayablePorts(port.ports)
|
||||
assert.Equal(t, port.expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ reclaimable: {{.Reclaimable}}
|
||||
}
|
||||
|
||||
func (ctx *DiskUsageContext) Write() (err error) {
|
||||
if ctx.Verbose == false {
|
||||
if !ctx.Verbose {
|
||||
ctx.buffer = bytes.NewBufferString("")
|
||||
ctx.preFormat()
|
||||
|
||||
@ -234,7 +234,6 @@ func (c *diskUsageImagesContext) Reclaimable() string {
|
||||
|
||||
type diskUsageContainersContext struct {
|
||||
HeaderContext
|
||||
verbose bool
|
||||
containers []*types.Container
|
||||
}
|
||||
|
||||
@ -297,7 +296,6 @@ func (c *diskUsageContainersContext) Reclaimable() string {
|
||||
|
||||
type diskUsageVolumesContext struct {
|
||||
HeaderContext
|
||||
verbose bool
|
||||
volumes []*types.Volume
|
||||
}
|
||||
|
||||
|
||||
@ -79,21 +79,18 @@ func (c *historyContext) ID() string {
|
||||
}
|
||||
|
||||
func (c *historyContext) CreatedAt() string {
|
||||
var created string
|
||||
created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0)))
|
||||
return created
|
||||
return units.HumanDuration(time.Now().UTC().Sub(time.Unix(c.h.Created, 0)))
|
||||
}
|
||||
|
||||
func (c *historyContext) CreatedSince() string {
|
||||
var created string
|
||||
created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0)))
|
||||
created := units.HumanDuration(time.Now().UTC().Sub(time.Unix(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 stringutils.Ellipsis(createdBy, 45)
|
||||
}
|
||||
return createdBy
|
||||
}
|
||||
|
||||
@ -234,12 +234,12 @@ func (c *imageContext) Digest() string {
|
||||
}
|
||||
|
||||
func (c *imageContext) CreatedSince() string {
|
||||
createdAt := time.Unix(int64(c.i.Created), 0)
|
||||
createdAt := time.Unix(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()
|
||||
return time.Unix(c.i.Created, 0).String()
|
||||
}
|
||||
|
||||
func (c *imageContext) Size() string {
|
||||
|
||||
@ -3,6 +3,7 @@ package formatter
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@ -10,6 +11,7 @@ import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNetworkContext(t *testing.T) {
|
||||
@ -183,12 +185,11 @@ func TestNetworkContextWriteJSON(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
msg := fmt.Sprintf("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)
|
||||
err := json.Unmarshal([]byte(line), &m)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, expectedJSONs[i], m, msg)
|
||||
}
|
||||
}
|
||||
|
||||
@ -203,11 +204,10 @@ func TestNetworkContextWriteJSONField(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
msg := fmt.Sprintf("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)
|
||||
err := json.Unmarshal([]byte(line), &s)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, networks[i].ID, s, msg)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package formatter
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -10,6 +11,7 @@ import (
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNodeContext(t *testing.T) {
|
||||
@ -248,12 +250,11 @@ func TestNodeContextWriteJSON(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
msg := fmt.Sprintf("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, testcase.expected[i], m)
|
||||
err := json.Unmarshal([]byte(line), &m)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, testcase.expected[i], m, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -269,12 +270,11 @@ func TestNodeContextWriteJSONField(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
msg := fmt.Sprintf("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)
|
||||
err := json.Unmarshal([]byte(line), &s)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, nodes[i].ID, s, msg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ func (d *dummy) Func1() string {
|
||||
return "Func1"
|
||||
}
|
||||
|
||||
func (d *dummy) func2() string {
|
||||
func (d *dummy) func2() string { // nolint: unused
|
||||
return "func2(should not be marshalled)"
|
||||
}
|
||||
|
||||
|
||||
@ -3,11 +3,13 @@ package formatter
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServiceContextWrite(t *testing.T) {
|
||||
@ -200,12 +202,11 @@ func TestServiceContextWriteJSON(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
msg := fmt.Sprintf("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)
|
||||
err := json.Unmarshal([]byte(line), &m)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, expectedJSONs[i], m, msg)
|
||||
}
|
||||
}
|
||||
func TestServiceContextWriteJSONField(t *testing.T) {
|
||||
@ -229,11 +230,10 @@ func TestServiceContextWriteJSONField(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
msg := fmt.Sprintf("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)
|
||||
err := json.Unmarshal([]byte(line), &s)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, services[i].Spec.Name, s, msg)
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,10 +109,8 @@ func NewStatsFormat(source, osType string) Format {
|
||||
}
|
||||
|
||||
// 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},
|
||||
}
|
||||
func NewContainerStats(container string) *ContainerStats {
|
||||
return &ContainerStats{StatsEntry: StatsEntry{Container: container}}
|
||||
}
|
||||
|
||||
// ContainerStatsWrite renders the context for a list of containers statistics
|
||||
|
||||
@ -3,12 +3,14 @@ package formatter
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVolumeContext(t *testing.T) {
|
||||
@ -153,12 +155,11 @@ func TestVolumeContextWriteJSON(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
msg := fmt.Sprintf("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)
|
||||
err := json.Unmarshal([]byte(line), &m)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, expectedJSONs[i], m, msg)
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,11 +174,10 @@ func TestVolumeContextWriteJSONField(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
msg := fmt.Sprintf("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)
|
||||
err := json.Unmarshal([]byte(line), &s)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, volumes[i].Name, s, msg)
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,6 @@ import (
|
||||
"github.com/docker/docker/pkg/progress"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"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"
|
||||
@ -291,9 +290,9 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
|
||||
Dockerfile: relDockerfile,
|
||||
ShmSize: options.shmSize.Value(),
|
||||
Ulimits: options.ulimits.GetList(),
|
||||
BuildArgs: runconfigopts.ConvertKVStringsToMapWithNil(options.buildArgs.GetAll()),
|
||||
BuildArgs: dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), options.buildArgs.GetAll()),
|
||||
AuthConfigs: authConfigs,
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
CacheFrom: options.cacheFrom,
|
||||
SecurityOpt: options.securityOpt,
|
||||
NetworkMode: options.networkMode,
|
||||
|
||||
@ -1,24 +1,23 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/builder/remotecontext/git"
|
||||
"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"
|
||||
@ -29,6 +28,8 @@ import (
|
||||
const (
|
||||
// DefaultDockerfileName is the Default filename with Docker commands, read by docker build
|
||||
DefaultDockerfileName string = "Dockerfile"
|
||||
// archiveHeaderSize is the number of bytes in an archive header
|
||||
archiveHeaderSize = 512
|
||||
)
|
||||
|
||||
// ValidateContextDirectory checks if all the contents of the directory
|
||||
@ -85,12 +86,12 @@ func ValidateContextDirectory(srcPath string, excludes []string) error {
|
||||
func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) {
|
||||
buf := bufio.NewReader(r)
|
||||
|
||||
magic, err := buf.Peek(archive.HeaderSize)
|
||||
magic, err := buf.Peek(archiveHeaderSize)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, "", errors.Errorf("failed to peek context header from STDIN: %v", err)
|
||||
}
|
||||
|
||||
if archive.IsArchive(magic) {
|
||||
if IsArchive(magic) {
|
||||
return ioutils.NewReadCloserWrapper(buf, func() error { return r.Close() }), dockerfileName, nil
|
||||
}
|
||||
|
||||
@ -134,6 +135,18 @@ func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCl
|
||||
|
||||
}
|
||||
|
||||
// IsArchive checks for the magic bytes of a tar or any supported compression
|
||||
// algorithm.
|
||||
func IsArchive(header []byte) bool {
|
||||
compression := archive.DetectCompression(header)
|
||||
if compression != archive.Uncompressed {
|
||||
return true
|
||||
}
|
||||
r := tar.NewReader(bytes.NewBuffer(header))
|
||||
_, err := r.Next()
|
||||
return err == 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
|
||||
@ -143,7 +156,7 @@ func GetContextFromGitURL(gitURL, dockerfileName string) (string, string, error)
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
return "", "", errors.Wrapf(err, "unable to find 'git'")
|
||||
}
|
||||
absContextDir, err := gitutils.Clone(gitURL)
|
||||
absContextDir, err := git.Clone(gitURL)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrapf(err, "unable to 'git clone' to temporary context directory")
|
||||
}
|
||||
@ -161,7 +174,7 @@ func GetContextFromGitURL(gitURL, dockerfileName string) (string, string, error)
|
||||
// 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)
|
||||
response, err := getWithStatusError(remoteURL)
|
||||
if err != nil {
|
||||
return nil, "", errors.Errorf("unable to download remote context %s: %v", remoteURL, err)
|
||||
}
|
||||
@ -173,6 +186,24 @@ func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.Read
|
||||
return GetContextFromReader(ioutils.NewReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName)
|
||||
}
|
||||
|
||||
// getWithStatusError does an http.Get() and returns an error if the
|
||||
// status code is 4xx or 5xx.
|
||||
func getWithStatusError(url string) (resp *http.Response, err error) {
|
||||
if resp, err = http.Get(url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode < 400 {
|
||||
return resp, nil
|
||||
}
|
||||
msg := fmt.Sprintf("failed to GET %s with status %s", url, resp.Status)
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, msg+": error reading body")
|
||||
}
|
||||
return nil, errors.Errorf(msg+": %s", bytes.TrimSpace(body))
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@ -266,3 +266,35 @@ func chdir(t *testing.T, dir string) func() {
|
||||
require.NoError(t, os.Chdir(dir))
|
||||
return func() { require.NoError(t, os.Chdir(workingDirectory)) }
|
||||
}
|
||||
|
||||
func TestIsArchive(t *testing.T) {
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
header []byte
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
doc: "nil is not a valid header",
|
||||
header: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
doc: "invalid header bytes",
|
||||
header: []byte{0x00, 0x01, 0x02},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
doc: "header for bzip2 archive",
|
||||
header: []byte{0x42, 0x5A, 0x68},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
doc: "header for 7zip archive is not supported",
|
||||
header: []byte{0x50, 0x4b, 0x03, 0x04},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, testcase := range testcases {
|
||||
assert.Equal(t, testcase.expected, IsArchive(testcase.header), testcase.doc)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,24 +6,17 @@ import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
"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 string
|
||||
args []string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "wrong-args",
|
||||
@ -57,10 +50,8 @@ func TestNewPullCommandErrors(t *testing.T) {
|
||||
|
||||
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 string
|
||||
args []string
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
|
||||
@ -7,15 +7,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
"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) {
|
||||
@ -60,11 +56,8 @@ func TestNewPushCommandErrors(t *testing.T) {
|
||||
|
||||
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 string
|
||||
args []string
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
|
||||
@ -56,8 +56,8 @@ func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error
|
||||
}
|
||||
|
||||
var errs []string
|
||||
for _, image := range images {
|
||||
dels, err := client.ImageRemove(ctx, image, options)
|
||||
for _, img := range images {
|
||||
dels, err := client.ImageRemove(ctx, img, options)
|
||||
if err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
} else {
|
||||
@ -72,7 +72,11 @@ func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.Errorf("%s", strings.Join(errs, "\n"))
|
||||
msg := strings.Join(errs, "\n")
|
||||
if !opts.force {
|
||||
return errors.New(msg)
|
||||
}
|
||||
fmt.Fprintf(dockerCli.Err(), msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -58,6 +58,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||
name string
|
||||
args []string
|
||||
imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error)
|
||||
expectedErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "Image Deleted",
|
||||
@ -67,6 +68,15 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||
return []types.ImageDeleteResponseItem{{Deleted: image}}, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Image Deleted with force option",
|
||||
args: []string{"-f", "image1"},
|
||||
imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) {
|
||||
assert.Equal(t, "image1", image)
|
||||
return []types.ImageDeleteResponseItem{}, errors.Errorf("error removing image")
|
||||
},
|
||||
expectedErrMsg: "error removing image",
|
||||
},
|
||||
{
|
||||
name: "Image Untagged",
|
||||
args: []string{"image1"},
|
||||
@ -88,14 +98,16 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{
|
||||
imageRemoveFunc: tc.imageRemoveFunc,
|
||||
}, buf))
|
||||
errBuf := new(bytes.Buffer)
|
||||
fakeCli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc}, buf)
|
||||
fakeCli.SetErr(errBuf)
|
||||
cmd := NewRemoveCommand(fakeCli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
if tc.expectedErrMsg != "" {
|
||||
assert.Equal(t, tc.expectedErrMsg, errBuf.String())
|
||||
}
|
||||
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)
|
||||
|
||||
@ -1,4 +1,2 @@
|
||||
Untagged: image1
|
||||
Deleted: image2
|
||||
Untagged: image1
|
||||
Deleted: image2
|
||||
|
||||
@ -1,2 +1 @@
|
||||
Deleted: image1
|
||||
Deleted: image1
|
||||
|
||||
@ -1,2 +1 @@
|
||||
Untagged: image1
|
||||
Untagged: image1
|
||||
|
||||
@ -110,6 +110,7 @@ func (i *TemplateInspector) tryRawInspectFallback(rawElement []byte) error {
|
||||
buffer := new(bytes.Buffer)
|
||||
rdr := bytes.NewReader(rawElement)
|
||||
dec := json.NewDecoder(rdr)
|
||||
dec.UseNumber()
|
||||
|
||||
if rawErr := dec.Decode(&raw); rawErr != nil {
|
||||
return errors.Errorf("unable to read inspect data: %v", rawErr)
|
||||
|
||||
@ -6,6 +6,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/templates"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testElement struct {
|
||||
@ -219,3 +221,39 @@ func TestIndentedInspectorRawElements(t *testing.T) {
|
||||
t.Fatalf("Expected `%s`, got `%s`", expected, b.String())
|
||||
}
|
||||
}
|
||||
|
||||
// moby/moby#32235
|
||||
// This test verifies that even if `tryRawInspectFallback` is called the fields containing
|
||||
// numerical values are displayed correctly.
|
||||
// For example, `docker inspect --format "{{.Id}} {{.Size}} alpine` and
|
||||
// `docker inspect --format "{{.ID}} {{.Size}} alpine" will have the same output which is
|
||||
// sha256:651aa95985aa4a17a38ffcf71f598ec461924ca96865facc2c5782ef2d2be07f 3983636
|
||||
func TestTemplateInspectorRawFallbackNumber(t *testing.T) {
|
||||
// Using typedElem to automatically fall to tryRawInspectFallback.
|
||||
typedElem := struct {
|
||||
ID string `json:"Id"`
|
||||
}{"ad3"}
|
||||
testcases := []struct {
|
||||
raw []byte
|
||||
exp string
|
||||
}{
|
||||
{raw: []byte(`{"Id": "ad3", "Size": 53317}`), exp: "53317 ad3\n"},
|
||||
{raw: []byte(`{"Id": "ad3", "Size": 53317.102}`), exp: "53317.102 ad3\n"},
|
||||
{raw: []byte(`{"Id": "ad3", "Size": 53317.0}`), exp: "53317.0 ad3\n"},
|
||||
}
|
||||
b := new(bytes.Buffer)
|
||||
tmpl, err := templates.Parse("{{.Size}} {{.Id}}")
|
||||
require.NoError(t, err)
|
||||
|
||||
i := NewTemplateInspector(b, tmpl)
|
||||
for _, tc := range testcases {
|
||||
err = i.Inspect(typedElem, tc.raw)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = i.Flush()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.exp, b.String())
|
||||
b.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ import (
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
@ -107,7 +106,7 @@ func runCreate(dockerCli *command.DockerCli, options createOptions) error {
|
||||
Ingress: options.ingress,
|
||||
Scope: options.scope,
|
||||
ConfigOnly: options.configOnly,
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
}
|
||||
|
||||
if from := options.configFrom; from != "" {
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -40,7 +41,7 @@ func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
getNetFunc := func(name string) (interface{}, []byte, error) {
|
||||
return client.NetworkInspectWithRaw(ctx, name, opts.verbose)
|
||||
return client.NetworkInspectWithRaw(ctx, name, types.NetworkInspectOptions{Verbose: opts.verbose})
|
||||
}
|
||||
|
||||
return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getNetFunc)
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -33,7 +34,7 @@ func runRemove(dockerCli *command.DockerCli, networks []string) error {
|
||||
status := 0
|
||||
|
||||
for _, name := range networks {
|
||||
if nw, _, err := client.NetworkInspectWithRaw(ctx, name, false); err == nil &&
|
||||
if nw, _, err := client.NetworkInspectWithRaw(ctx, name, types.NetworkInspectOptions{}); err == nil &&
|
||||
nw.Ingress &&
|
||||
!command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), ingressWarning) {
|
||||
continue
|
||||
|
||||
@ -11,7 +11,6 @@ type nodeOptions struct {
|
||||
}
|
||||
|
||||
type annotations struct {
|
||||
name string
|
||||
labels opts.ListOpts
|
||||
}
|
||||
|
||||
|
||||
@ -103,11 +103,11 @@ func TestNodePs(t *testing.T) {
|
||||
},
|
||||
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
|
||||
return []swarm.Task{
|
||||
*Task(TaskID("taskID1"), ServiceID("failure"),
|
||||
*Task(TaskID("taskID1"), TaskServiceID("failure"),
|
||||
WithStatus(Timestamp(time.Now().Add(-2*time.Hour)), StatusErr("a task error"))),
|
||||
*Task(TaskID("taskID2"), ServiceID("failure"),
|
||||
*Task(TaskID("taskID2"), TaskServiceID("failure"),
|
||||
WithStatus(Timestamp(time.Now().Add(-3*time.Hour)), StatusErr("a task error"))),
|
||||
*Task(TaskID("taskID3"), ServiceID("failure"),
|
||||
*Task(TaskID("taskID3"), TaskServiceID("failure"),
|
||||
WithStatus(Timestamp(time.Now().Add(-4*time.Hour)), StatusErr("a task error"))),
|
||||
}, nil
|
||||
},
|
||||
|
||||
@ -7,7 +7,6 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@ -95,7 +94,7 @@ func mergeNodeUpdate(flags *pflag.FlagSet) func(*swarm.Node) error {
|
||||
}
|
||||
if flags.Changed(flagLabelAdd) {
|
||||
labels := flags.Lookup(flagLabelAdd).Value.(*opts.ListOpts).GetAll()
|
||||
for k, v := range runconfigopts.ConvertKVStringsToMap(labels) {
|
||||
for k, v := range opts.ConvertKVStringsToMap(labels) {
|
||||
spec.Annotations.Labels[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,6 @@ type loginOptions struct {
|
||||
serverAddress string
|
||||
user string
|
||||
password string
|
||||
email string
|
||||
}
|
||||
|
||||
// NewLoginCommand creates a new `docker login` command
|
||||
|
||||
@ -10,7 +10,6 @@ import (
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"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"
|
||||
@ -65,7 +64,7 @@ func runSecretCreate(dockerCli command.Cli, options createOptions) error {
|
||||
spec := swarm.SecretSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: options.name,
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
},
|
||||
Data: secretData,
|
||||
}
|
||||
|
||||
@ -76,6 +76,7 @@ func TestSecretRemoveContinueAfterError(t *testing.T) {
|
||||
}, buf)
|
||||
|
||||
cmd := newSecretRemoveCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(names)
|
||||
assert.EqualError(t, cmd.Execute(), "error removing secret: foo")
|
||||
assert.Equal(t, names, removedSecrets)
|
||||
|
||||
@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
// waitOnService waits for the service to converge. It outputs a progress bar,
|
||||
// if appopriate based on the CLI flags.
|
||||
// if appropriate 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()
|
||||
|
||||
@ -61,7 +61,7 @@ func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
|
||||
}
|
||||
|
||||
getNetwork := func(ref string) (interface{}, []byte, error) {
|
||||
network, _, err := client.NetworkInspectWithRaw(ctx, ref, false)
|
||||
network, _, err := client.NetworkInspectWithRaw(ctx, ref, types.NetworkInspectOptions{Scope: "swarm"})
|
||||
if err == nil || !apiclient.IsErrNetworkNotFound(err) {
|
||||
return network, nil, err
|
||||
}
|
||||
|
||||
@ -117,11 +117,7 @@ func TestJSONFormatWithNoUpdateConfig(t *testing.T) {
|
||||
// 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)
|
||||
@ -130,11 +126,9 @@ func TestJSONFormatWithNoUpdateConfig(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -164,7 +164,7 @@ func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error {
|
||||
|
||||
// getMaxLength gets the maximum length of the number in base 10
|
||||
func getMaxLength(i int) int {
|
||||
return len(strconv.FormatInt(int64(i), 10))
|
||||
return len(strconv.Itoa(i))
|
||||
}
|
||||
|
||||
type taskFormatter struct {
|
||||
@ -237,7 +237,7 @@ type logWriter struct {
|
||||
func (lw *logWriter) Write(buf []byte) (int, error) {
|
||||
// this works but ONLY because stdcopy calls write a whole line at a time.
|
||||
// if this ends up horribly broken or panics, check to see if stdcopy has
|
||||
// reneged on that asssumption. (@god forgive me)
|
||||
// reneged on that assumption. (@god forgive me)
|
||||
// also this only works because the logs format is, like, barely parsable.
|
||||
// if something changes in the logs format, this is gonna break
|
||||
|
||||
|
||||
@ -8,10 +8,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
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"
|
||||
@ -350,11 +350,15 @@ func convertNetworks(ctx context.Context, apiClient client.NetworkAPIClient, net
|
||||
var netAttach []swarm.NetworkAttachmentConfig
|
||||
for _, net := range networks.Value() {
|
||||
networkIDOrName := net.Target
|
||||
_, err := apiClient.NetworkInspect(ctx, networkIDOrName, false)
|
||||
_, err := apiClient.NetworkInspect(ctx, networkIDOrName, types.NetworkInspectOptions{Scope: "swarm"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
netAttach = append(netAttach, swarm.NetworkAttachmentConfig{Target: net.Target, Aliases: net.Aliases, DriverOpts: net.DriverOpts})
|
||||
netAttach = append(netAttach, swarm.NetworkAttachmentConfig{ // nolint: gosimple
|
||||
Target: net.Target,
|
||||
Aliases: net.Aliases,
|
||||
DriverOpts: net.DriverOpts,
|
||||
})
|
||||
}
|
||||
sort.Sort(byNetworkTarget(netAttach))
|
||||
return netAttach, nil
|
||||
@ -389,7 +393,7 @@ func (ldo *logDriverOptions) toLogDriver() *swarm.Driver {
|
||||
// set the log driver only if specified.
|
||||
return &swarm.Driver{
|
||||
Name: ldo.name,
|
||||
Options: runconfigopts.ConvertKVStringsToMap(ldo.opts.GetAll()),
|
||||
Options: opts.ConvertKVStringsToMap(ldo.opts.GetAll()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -519,36 +523,36 @@ func newServiceOptions() *serviceOptions {
|
||||
}
|
||||
}
|
||||
|
||||
func (opts *serviceOptions) ToServiceMode() (swarm.ServiceMode, error) {
|
||||
func (options *serviceOptions) ToServiceMode() (swarm.ServiceMode, error) {
|
||||
serviceMode := swarm.ServiceMode{}
|
||||
switch opts.mode {
|
||||
switch options.mode {
|
||||
case "global":
|
||||
if opts.replicas.Value() != nil {
|
||||
if options.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(),
|
||||
Replicas: options.replicas.Value(),
|
||||
}
|
||||
default:
|
||||
return serviceMode, errors.Errorf("Unknown mode: %s, only replicated and global supported", opts.mode)
|
||||
return serviceMode, errors.Errorf("Unknown mode: %s, only replicated and global supported", options.mode)
|
||||
}
|
||||
return serviceMode, nil
|
||||
}
|
||||
|
||||
func (opts *serviceOptions) ToStopGracePeriod(flags *pflag.FlagSet) *time.Duration {
|
||||
func (options *serviceOptions) ToStopGracePeriod(flags *pflag.FlagSet) *time.Duration {
|
||||
if flags.Changed(flagStopGracePeriod) {
|
||||
return opts.stopGrace.Value()
|
||||
return options.stopGrace.Value()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (opts *serviceOptions) ToService(ctx context.Context, apiClient client.NetworkAPIClient, flags *pflag.FlagSet) (swarm.ServiceSpec, error) {
|
||||
func (options *serviceOptions) ToService(ctx context.Context, apiClient client.NetworkAPIClient, flags *pflag.FlagSet) (swarm.ServiceSpec, error) {
|
||||
var service swarm.ServiceSpec
|
||||
|
||||
envVariables, err := runconfigopts.ReadKVStrings(opts.envFile.GetAll(), opts.env.GetAll())
|
||||
envVariables, err := opts.ReadKVStrings(options.envFile.GetAll(), options.env.GetAll())
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
@ -567,68 +571,68 @@ func (opts *serviceOptions) ToService(ctx context.Context, apiClient client.Netw
|
||||
currentEnv = append(currentEnv, env)
|
||||
}
|
||||
|
||||
healthConfig, err := opts.healthcheck.toHealthConfig()
|
||||
healthConfig, err := options.healthcheck.toHealthConfig()
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
|
||||
serviceMode, err := opts.ToServiceMode()
|
||||
serviceMode, err := options.ToServiceMode()
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
|
||||
networks, err := convertNetworks(ctx, apiClient, opts.networks)
|
||||
networks, err := convertNetworks(ctx, apiClient, options.networks)
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
|
||||
service = swarm.ServiceSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: opts.name,
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()),
|
||||
Name: options.name,
|
||||
Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
},
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: swarm.ContainerSpec{
|
||||
Image: opts.image,
|
||||
Args: opts.args,
|
||||
Command: opts.entrypoint.Value(),
|
||||
Image: options.image,
|
||||
Args: options.args,
|
||||
Command: options.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(),
|
||||
Hostname: options.hostname,
|
||||
Labels: opts.ConvertKVStringsToMap(options.containerLabels.GetAll()),
|
||||
Dir: options.workdir,
|
||||
User: options.user,
|
||||
Groups: options.groups.GetAll(),
|
||||
StopSignal: options.stopSignal,
|
||||
TTY: options.tty,
|
||||
ReadOnly: options.readOnly,
|
||||
Mounts: options.mounts.Value(),
|
||||
DNSConfig: &swarm.DNSConfig{
|
||||
Nameservers: opts.dns.GetAll(),
|
||||
Search: opts.dnsSearch.GetAll(),
|
||||
Options: opts.dnsOption.GetAll(),
|
||||
Nameservers: options.dns.GetAll(),
|
||||
Search: options.dnsSearch.GetAll(),
|
||||
Options: options.dnsOption.GetAll(),
|
||||
},
|
||||
Hosts: convertExtraHostsToSwarmHosts(opts.hosts.GetAll()),
|
||||
StopGracePeriod: opts.ToStopGracePeriod(flags),
|
||||
Hosts: convertExtraHostsToSwarmHosts(options.hosts.GetAll()),
|
||||
StopGracePeriod: options.ToStopGracePeriod(flags),
|
||||
Healthcheck: healthConfig,
|
||||
},
|
||||
Networks: networks,
|
||||
Resources: opts.resources.ToResourceRequirements(),
|
||||
RestartPolicy: opts.restartPolicy.ToRestartPolicy(flags),
|
||||
Resources: options.resources.ToResourceRequirements(),
|
||||
RestartPolicy: options.restartPolicy.ToRestartPolicy(flags),
|
||||
Placement: &swarm.Placement{
|
||||
Constraints: opts.constraints.GetAll(),
|
||||
Preferences: opts.placementPrefs.prefs,
|
||||
Constraints: options.constraints.GetAll(),
|
||||
Preferences: options.placementPrefs.prefs,
|
||||
},
|
||||
LogDriver: opts.logDriver.toLogDriver(),
|
||||
LogDriver: options.logDriver.toLogDriver(),
|
||||
},
|
||||
Mode: serviceMode,
|
||||
UpdateConfig: opts.update.updateConfig(flags),
|
||||
RollbackConfig: opts.update.rollbackConfig(flags),
|
||||
EndpointSpec: opts.endpoint.ToEndpointSpec(),
|
||||
UpdateConfig: options.update.updateConfig(flags),
|
||||
RollbackConfig: options.update.rollbackConfig(flags),
|
||||
EndpointSpec: options.endpoint.ToEndpointSpec(),
|
||||
}
|
||||
|
||||
if opts.credentialSpec.Value() != nil {
|
||||
if options.credentialSpec.Value() != nil {
|
||||
service.TaskTemplate.ContainerSpec.Privileges = &swarm.Privileges{
|
||||
CredentialSpec: opts.credentialSpec.Value(),
|
||||
CredentialSpec: options.credentialSpec.Value(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,10 @@ import (
|
||||
// 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) {
|
||||
if len(requestedSecrets) == 0 {
|
||||
return []*swarmtypes.SecretReference{}, nil
|
||||
}
|
||||
|
||||
secretRefs := make(map[string]*swarmtypes.SecretReference)
|
||||
ctx := context.Background()
|
||||
|
||||
@ -61,6 +65,10 @@ func ParseSecrets(client client.SecretAPIClient, requestedSecrets []*swarmtypes.
|
||||
// ParseConfigs retrieves the configs from the requested names and converts
|
||||
// them to config references to use with the spec
|
||||
func ParseConfigs(client client.ConfigAPIClient, requestedConfigs []*swarmtypes.ConfigReference) ([]*swarmtypes.ConfigReference, error) {
|
||||
if len(requestedConfigs) == 0 {
|
||||
return []*swarmtypes.ConfigReference{}, nil
|
||||
}
|
||||
|
||||
configRefs := make(map[string]*swarmtypes.ConfigReference)
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
@ -233,7 +233,7 @@ func writeOverallProgress(progressOut progress.Output, numerator, denominator in
|
||||
type replicatedProgressUpdater struct {
|
||||
progressOut progress.Output
|
||||
|
||||
// used for maping slots to a contiguous space
|
||||
// used for mapping slots to a contiguous space
|
||||
// this also causes progress bars to appear in order
|
||||
slotMap map[int]int
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
@ -52,57 +53,10 @@ func runPS(dockerCli command.Cli, options psOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
filter := options.filter.Value()
|
||||
|
||||
serviceIDFilter := filters.NewArgs()
|
||||
serviceNameFilter := filters.NewArgs()
|
||||
for _, service := range options.services {
|
||||
serviceIDFilter.Add("id", service)
|
||||
serviceNameFilter.Add("name", service)
|
||||
}
|
||||
serviceByIDList, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceIDFilter})
|
||||
filter, notfound, err := createFilter(ctx, client, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serviceByNameList, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceNameFilter})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, service := range options.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 {
|
||||
@ -117,6 +71,80 @@ func runPS(dockerCli command.Cli, options psOptions) error {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
return task.Print(ctx, dockerCli, tasks, idresolver.New(client, options.noResolve), !options.noTrunc, options.quiet, format)
|
||||
if err := task.Print(ctx, dockerCli, tasks, idresolver.New(client, options.noResolve), !options.noTrunc, options.quiet, format); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(notfound) != 0 {
|
||||
return errors.New(strings.Join(notfound, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createFilter(ctx context.Context, client client.APIClient, options psOptions) (filters.Args, []string, error) {
|
||||
filter := options.filter.Value()
|
||||
|
||||
serviceIDFilter := filters.NewArgs()
|
||||
serviceNameFilter := filters.NewArgs()
|
||||
for _, service := range options.services {
|
||||
serviceIDFilter.Add("id", service)
|
||||
serviceNameFilter.Add("name", service)
|
||||
}
|
||||
serviceByIDList, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceIDFilter})
|
||||
if err != nil {
|
||||
return filter, nil, err
|
||||
}
|
||||
serviceByNameList, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceNameFilter})
|
||||
if err != nil {
|
||||
return filter, nil, err
|
||||
}
|
||||
|
||||
var notfound []string
|
||||
serviceCount := 0
|
||||
loop:
|
||||
// Match services by 1. Full ID, 2. Full name, 3. ID prefix. An error is returned if the ID-prefix match is ambiguous
|
||||
for _, service := range options.services {
|
||||
for _, s := range serviceByIDList {
|
||||
if s.ID == service {
|
||||
filter.Add("service", s.ID)
|
||||
serviceCount++
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
for _, s := range serviceByNameList {
|
||||
if s.Spec.Annotations.Name == service {
|
||||
filter.Add("service", s.ID)
|
||||
serviceCount++
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
found := false
|
||||
for _, s := range serviceByIDList {
|
||||
if strings.HasPrefix(s.ID, service) {
|
||||
if found {
|
||||
return filter, nil, errors.New("multiple services found with provided prefix: " + service)
|
||||
}
|
||||
filter.Add("service", s.ID)
|
||||
serviceCount++
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
notfound = append(notfound, "no such service: "+service)
|
||||
}
|
||||
}
|
||||
if serviceCount == 0 {
|
||||
return filter, nil, errors.New(strings.Join(notfound, "\n"))
|
||||
}
|
||||
if filter.Include("node") {
|
||||
nodeFilters := filter.Get("node")
|
||||
for _, nodeFilter := range nodeFilters {
|
||||
nodeReference, err := node.Reference(ctx, client, nodeFilter)
|
||||
if err != nil {
|
||||
return filter, nil, err
|
||||
}
|
||||
filter.Del("node", nodeFilter)
|
||||
filter.Add("node", nodeReference)
|
||||
}
|
||||
}
|
||||
return filter, notfound, err
|
||||
}
|
||||
|
||||
118
components/cli/cli/command/service/ps_test.go
Normal file
118
components/cli/cli/command/service/ps_test.go
Normal file
@ -0,0 +1,118 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"bytes"
|
||||
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/cli/opts"
|
||||
"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/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
serviceListFunc func(context.Context, types.ServiceListOptions) ([]swarm.Service, error)
|
||||
}
|
||||
|
||||
func (f *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
if f.serviceListFunc != nil {
|
||||
return f.serviceListFunc(ctx, options)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func newService(id string, name string) swarm.Service {
|
||||
return swarm.Service{
|
||||
ID: id,
|
||||
Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: name}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateFilter(t *testing.T) {
|
||||
client := &fakeClient{
|
||||
serviceListFunc: func(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{
|
||||
{ID: "idmatch"},
|
||||
{ID: "idprefixmatch"},
|
||||
newService("cccccccc", "namematch"),
|
||||
newService("01010101", "notfoundprefix"),
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
filter := opts.NewFilterOpt()
|
||||
require.NoError(t, filter.Set("node=somenode"))
|
||||
options := psOptions{
|
||||
services: []string{"idmatch", "idprefix", "namematch", "notfound"},
|
||||
filter: filter,
|
||||
}
|
||||
|
||||
actual, notfound, err := createFilter(context.Background(), client, options)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, notfound, []string{"no such service: notfound"})
|
||||
|
||||
expected := filters.NewArgs()
|
||||
expected.Add("service", "idmatch")
|
||||
expected.Add("service", "idprefixmatch")
|
||||
expected.Add("service", "cccccccc")
|
||||
expected.Add("node", "somenode")
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestCreateFilterWithAmbiguousIDPrefixError(t *testing.T) {
|
||||
client := &fakeClient{
|
||||
serviceListFunc: func(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{
|
||||
{ID: "aaaone"},
|
||||
{ID: "aaatwo"},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
options := psOptions{
|
||||
services: []string{"aaa"},
|
||||
filter: opts.NewFilterOpt(),
|
||||
}
|
||||
_, _, err := createFilter(context.Background(), client, options)
|
||||
assert.EqualError(t, err, "multiple services found with provided prefix: aaa")
|
||||
}
|
||||
|
||||
func TestCreateFilterNoneFound(t *testing.T) {
|
||||
client := &fakeClient{}
|
||||
options := psOptions{
|
||||
services: []string{"foo", "notfound"},
|
||||
filter: opts.NewFilterOpt(),
|
||||
}
|
||||
_, _, err := createFilter(context.Background(), client, options)
|
||||
assert.EqualError(t, err, "no such service: foo\nno such service: notfound")
|
||||
}
|
||||
|
||||
func TestRunPSWarnsOnNotFound(t *testing.T) {
|
||||
client := &fakeClient{
|
||||
serviceListFunc: func(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{
|
||||
{ID: "foo"},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(client, out)
|
||||
options := psOptions{
|
||||
services: []string{"foo", "bar"},
|
||||
filter: opts.NewFilterOpt(),
|
||||
format: "{{.ID}}",
|
||||
}
|
||||
err := runPS(cli, options)
|
||||
assert.EqualError(t, err, "no such service: bar")
|
||||
}
|
||||
@ -15,7 +15,6 @@ import (
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/client"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/docker/swarmkit/api/defaults"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@ -504,9 +503,8 @@ func updatePlacementPreferences(flags *pflag.FlagSet, placement *swarm.Placement
|
||||
}
|
||||
|
||||
if flags.Changed(flagPlacementPrefAdd) {
|
||||
for _, addition := range flags.Lookup(flagPlacementPrefAdd).Value.(*placementPrefOpts).prefs {
|
||||
newPrefs = append(newPrefs, addition)
|
||||
}
|
||||
newPrefs = append(newPrefs,
|
||||
flags.Lookup(flagPlacementPrefAdd).Value.(*placementPrefOpts).prefs...)
|
||||
}
|
||||
|
||||
placement.Preferences = newPrefs
|
||||
@ -519,7 +517,7 @@ func updateContainerLabels(flags *pflag.FlagSet, field *map[string]string) {
|
||||
}
|
||||
|
||||
values := flags.Lookup(flagContainerLabelAdd).Value.(*opts.ListOpts).GetAll()
|
||||
for key, value := range runconfigopts.ConvertKVStringsToMap(values) {
|
||||
for key, value := range opts.ConvertKVStringsToMap(values) {
|
||||
(*field)[key] = value
|
||||
}
|
||||
}
|
||||
@ -539,7 +537,7 @@ func updateLabels(flags *pflag.FlagSet, field *map[string]string) {
|
||||
}
|
||||
|
||||
values := flags.Lookup(flagLabelAdd).Value.(*opts.ListOpts).GetAll()
|
||||
for key, value := range runconfigopts.ConvertKVStringsToMap(values) {
|
||||
for key, value := range opts.ConvertKVStringsToMap(values) {
|
||||
(*field)[key] = value
|
||||
}
|
||||
}
|
||||
@ -933,7 +931,7 @@ func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error {
|
||||
|
||||
taskTemplate.LogDriver = &swarm.Driver{
|
||||
Name: name,
|
||||
Options: runconfigopts.ConvertKVStringsToMap(flags.Lookup(flagLogOpt).Value.(*opts.ListOpts).GetAll()),
|
||||
Options: opts.ConvertKVStringsToMap(flags.Lookup(flagLogOpt).Value.(*opts.ListOpts).GetAll()),
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -1009,7 +1007,7 @@ func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flag
|
||||
toRemove := buildToRemoveSet(flags, flagNetworkRemove)
|
||||
idsToRemove := make(map[string]struct{})
|
||||
for networkIDOrName := range toRemove {
|
||||
network, err := apiClient.NetworkInspect(ctx, networkIDOrName, false)
|
||||
network, err := apiClient.NetworkInspect(ctx, networkIDOrName, types.NetworkInspectOptions{Scope: "swarm"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/compose/convert"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
@ -24,14 +25,24 @@ type fakeClient struct {
|
||||
removedSecrets []string
|
||||
removedConfigs []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)
|
||||
configListFunc func(options types.ConfigListOptions) ([]swarm.Config, error)
|
||||
serviceRemoveFunc func(serviceID string) error
|
||||
networkRemoveFunc func(networkID string) error
|
||||
secretRemoveFunc func(secretID string) error
|
||||
configRemoveFunc func(configID string) error
|
||||
serviceListFunc func(options types.ServiceListOptions) ([]swarm.Service, error)
|
||||
networkListFunc func(options types.NetworkListOptions) ([]types.NetworkResource, error)
|
||||
secretListFunc func(options types.SecretListOptions) ([]swarm.Secret, error)
|
||||
configListFunc func(options types.ConfigListOptions) ([]swarm.Config, error)
|
||||
nodeListFunc func(options types.NodeListOptions) ([]swarm.Node, error)
|
||||
taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error)
|
||||
nodeInspectWithRaw func(ref string) (swarm.Node, []byte, error)
|
||||
serviceRemoveFunc func(serviceID string) error
|
||||
networkRemoveFunc func(networkID string) error
|
||||
secretRemoveFunc func(secretID string) error
|
||||
configRemoveFunc func(configID string) error
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) {
|
||||
return types.Version{
|
||||
Version: "docker-dev",
|
||||
APIVersion: api.DefaultVersion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
@ -94,6 +105,27 @@ func (cli *fakeClient) ConfigList(ctx context.Context, options types.ConfigListO
|
||||
return configsList, 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
|
||||
}
|
||||
|
||||
func (cli *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
|
||||
if cli.nodeListFunc != nil {
|
||||
return cli.nodeListFunc(options)
|
||||
}
|
||||
return []swarm.Node{}, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, ref string) (swarm.Node, []byte, error) {
|
||||
if cli.nodeInspectWithRaw != nil {
|
||||
return cli.nodeInspectWithRaw(ref)
|
||||
}
|
||||
return swarm.Node{}, nil, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ServiceRemove(ctx context.Context, serviceID string) error {
|
||||
if cli.serviceRemoveFunc != nil {
|
||||
return cli.serviceRemoveFunc(serviceID)
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/compose/convert"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
@ -14,12 +15,16 @@ import (
|
||||
|
||||
const (
|
||||
defaultNetworkDriver = "overlay"
|
||||
resolveImageAlways = "always"
|
||||
resolveImageChanged = "changed"
|
||||
resolveImageNever = "never"
|
||||
)
|
||||
|
||||
type deployOptions struct {
|
||||
bundlefile string
|
||||
composefile string
|
||||
namespace string
|
||||
resolveImage string
|
||||
sendRegistryAuth bool
|
||||
prune bool
|
||||
}
|
||||
@ -44,12 +49,19 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command {
|
||||
addRegistryAuthFlag(&opts.sendRegistryAuth, flags)
|
||||
flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced")
|
||||
flags.SetAnnotation("prune", "version", []string{"1.27"})
|
||||
flags.StringVar(&opts.resolveImage, "resolve-image", resolveImageAlways,
|
||||
`Query the registry to resolve image digest and supported platforms ("`+resolveImageAlways+`"|"`+resolveImageChanged+`"|"`+resolveImageNever+`")`)
|
||||
flags.SetAnnotation("resolve-image", "version", []string{"1.30"})
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runDeploy(dockerCli command.Cli, opts deployOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
if err := validateResolveImageFlag(dockerCli, &opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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).")
|
||||
@ -62,6 +74,20 @@ func runDeploy(dockerCli command.Cli, opts deployOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
// validateResolveImageFlag validates the opts.resolveImage command line option
|
||||
// and also turns image resolution off if the version is older than 1.30
|
||||
func validateResolveImageFlag(dockerCli command.Cli, opts *deployOptions) error {
|
||||
if opts.resolveImage != resolveImageAlways && opts.resolveImage != resolveImageChanged && opts.resolveImage != resolveImageNever {
|
||||
return errors.Errorf("Invalid option %s for flag --resolve-image", opts.resolveImage)
|
||||
}
|
||||
// client side image resolution should not be done when the supported
|
||||
// server version is older than 1.30
|
||||
if versions.LessThan(dockerCli.Client().ClientVersion(), "1.30") {
|
||||
opts.resolveImage = resolveImageNever
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@ -87,5 +87,5 @@ func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions
|
||||
if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
|
||||
return err
|
||||
}
|
||||
return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth)
|
||||
return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.resolveImage)
|
||||
}
|
||||
|
||||
@ -92,7 +92,7 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOption
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth)
|
||||
return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.resolveImage)
|
||||
}
|
||||
|
||||
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
|
||||
@ -134,10 +134,7 @@ func getConfigDetails(composefile string) (composetypes.ConfigDetails, error) {
|
||||
// TODO: support multiple files
|
||||
details.ConfigFiles = []composetypes.ConfigFile{*configFile}
|
||||
details.Environment, err = buildEnvironment(os.Environ())
|
||||
if err != nil {
|
||||
return details, err
|
||||
}
|
||||
return details, nil
|
||||
return details, err
|
||||
}
|
||||
|
||||
func buildEnvironment(env []string) (map[string]string, error) {
|
||||
@ -174,7 +171,7 @@ func validateExternalNetworks(
|
||||
externalNetworks []string,
|
||||
) error {
|
||||
for _, networkName := range externalNetworks {
|
||||
network, err := client.NetworkInspect(ctx, networkName, false)
|
||||
network, err := client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{})
|
||||
switch {
|
||||
case dockerclient.IsErrNotFound(err):
|
||||
return errors.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed", networkName)
|
||||
@ -196,17 +193,18 @@ func createSecrets(
|
||||
|
||||
for _, secretSpec := range secrets {
|
||||
secret, _, err := client.SecretInspectWithRaw(ctx, secretSpec.Name)
|
||||
if err == nil {
|
||||
switch {
|
||||
case err == nil:
|
||||
// secret already exists, then we update that
|
||||
if err := client.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil {
|
||||
return err
|
||||
return errors.Wrapf(err, "failed to update secret %s", secretSpec.Name)
|
||||
}
|
||||
} else if apiclient.IsErrSecretNotFound(err) {
|
||||
case apiclient.IsErrSecretNotFound(err):
|
||||
// secret does not exist, then we create a new one.
|
||||
if _, err := client.SecretCreate(ctx, secretSpec); err != nil {
|
||||
return err
|
||||
return errors.Wrapf(err, "failed to create secret %s", secretSpec.Name)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -222,17 +220,18 @@ func createConfigs(
|
||||
|
||||
for _, configSpec := range configs {
|
||||
config, _, err := client.ConfigInspectWithRaw(ctx, configSpec.Name)
|
||||
if err == nil {
|
||||
switch {
|
||||
case err == nil:
|
||||
// config already exists, then we update that
|
||||
if err := client.ConfigUpdate(ctx, config.ID, config.Meta.Version, configSpec); err != nil {
|
||||
return err
|
||||
errors.Wrapf(err, "failed to update config %s", configSpec.Name)
|
||||
}
|
||||
} else if apiclient.IsErrConfigNotFound(err) {
|
||||
case apiclient.IsErrConfigNotFound(err):
|
||||
// config does not exist, then we create a new one.
|
||||
if _, err := client.ConfigCreate(ctx, configSpec); err != nil {
|
||||
return err
|
||||
errors.Wrapf(err, "failed to create config %s", configSpec.Name)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -269,10 +268,9 @@ func createNetworks(
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name)
|
||||
if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil {
|
||||
return err
|
||||
return errors.Wrapf(err, "failed to create network %s", internalName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -282,6 +280,7 @@ func deployServices(
|
||||
services map[string]swarm.ServiceSpec,
|
||||
namespace convert.Namespace,
|
||||
sendAuth bool,
|
||||
resolveImage string,
|
||||
) error {
|
||||
apiClient := dockerCli.Client()
|
||||
out := dockerCli.Out()
|
||||
@ -300,9 +299,9 @@ func deployServices(
|
||||
name := namespace.Scope(internalName)
|
||||
|
||||
encodedAuth := ""
|
||||
image := serviceSpec.TaskTemplate.ContainerSpec.Image
|
||||
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
|
||||
@ -312,10 +311,12 @@ func deployServices(
|
||||
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
|
||||
updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
|
||||
|
||||
if resolveImage == resolveImageAlways || (resolveImage == resolveImageChanged && image != service.Spec.Labels[convert.LabelImage]) {
|
||||
updateOpts.QueryRegistry = true
|
||||
}
|
||||
|
||||
response, err := apiClient.ServiceUpdate(
|
||||
ctx,
|
||||
service.ID,
|
||||
@ -324,7 +325,7 @@ func deployServices(
|
||||
updateOpts,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrapf(err, "failed to update service %s", name)
|
||||
}
|
||||
|
||||
for _, warning := range response.Warnings {
|
||||
@ -333,15 +334,17 @@ func deployServices(
|
||||
} else {
|
||||
fmt.Fprintf(out, "Creating service %s\n", name)
|
||||
|
||||
createOpts := types.ServiceCreateOptions{}
|
||||
if sendAuth {
|
||||
createOpts.EncodedRegistryAuth = encodedAuth
|
||||
createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
|
||||
|
||||
// query registry if flag disabling it was not set
|
||||
if resolveImage == resolveImageAlways || resolveImage == resolveImageChanged {
|
||||
createOpts.QueryRegistry = true
|
||||
}
|
||||
|
||||
if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil {
|
||||
return err
|
||||
return errors.Wrapf(err, "failed to create service %s", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -70,7 +70,7 @@ func TestValidateExternalNetworks(t *testing.T) {
|
||||
|
||||
for _, testcase := range testcases {
|
||||
fakeClient := &network.FakeClient{
|
||||
NetworkInspectFunc: func(_ context.Context, _ string, _ bool) (types.NetworkResource, error) {
|
||||
NetworkInspectFunc: func(_ context.Context, _ string, _ types.NetworkInspectOptions) (types.NetworkResource, error) {
|
||||
return testcase.inspectResponse, testcase.inspectError
|
||||
},
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ type listOptions struct {
|
||||
format string
|
||||
}
|
||||
|
||||
func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts := listOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -36,7 +36,7 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
||||
func runList(dockerCli command.Cli, opts listOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
@ -69,7 +69,7 @@ func getStacks(ctx context.Context, apiclient client.APIClient) ([]*formatter.St
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]*formatter.Stack, 0)
|
||||
m := make(map[string]*formatter.Stack)
|
||||
for _, service := range services {
|
||||
labels := service.Spec.Labels
|
||||
name, ok := labels[convert.LabelNamespace]
|
||||
|
||||
122
components/cli/cli/command/stack/list_test.go
Normal file
122
components/cli/cli/command/stack/list_test.go
Normal file
@ -0,0 +1,122 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
// Import builders to get the builder function as package function
|
||||
. "github.com/docker/cli/cli/internal/test/builders"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/pkg/testutil/golden"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestListErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
flags map[string]string
|
||||
serviceListFunc func(options types.ServiceListOptions) ([]swarm.Service, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"foo"},
|
||||
expectedError: "accepts no argument",
|
||||
},
|
||||
{
|
||||
flags: map[string]string{
|
||||
"format": "{{invalid format}}",
|
||||
},
|
||||
expectedError: "Template parsing error",
|
||||
},
|
||||
{
|
||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{}, errors.Errorf("error getting services")
|
||||
},
|
||||
expectedError: "error getting services",
|
||||
},
|
||||
{
|
||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{*Service()}, nil
|
||||
},
|
||||
expectedError: "cannot get label",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cmd := newListCommand(test.NewFakeCli(&fakeClient{
|
||||
serviceListFunc: tc.serviceListFunc,
|
||||
}, &bytes.Buffer{}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
for key, value := range tc.flags {
|
||||
cmd.Flags().Set(key, value)
|
||||
}
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListWithFormat(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newListCommand(test.NewFakeCli(&fakeClient{
|
||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{
|
||||
*Service(
|
||||
ServiceLabels(map[string]string{
|
||||
"com.docker.stack.namespace": "service-name-foo",
|
||||
}),
|
||||
)}, nil
|
||||
},
|
||||
}, buf))
|
||||
cmd.Flags().Set("format", "{{ .Name }}")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "stack-list-with-format.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestListWithoutFormat(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newListCommand(test.NewFakeCli(&fakeClient{
|
||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{
|
||||
*Service(
|
||||
ServiceLabels(map[string]string{
|
||||
"com.docker.stack.namespace": "service-name-foo",
|
||||
}),
|
||||
)}, nil
|
||||
},
|
||||
}, buf))
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "stack-list-without-format.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestListOrder(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newListCommand(test.NewFakeCli(&fakeClient{
|
||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{
|
||||
*Service(
|
||||
ServiceLabels(map[string]string{
|
||||
"com.docker.stack.namespace": "service-name-foo",
|
||||
}),
|
||||
),
|
||||
*Service(
|
||||
ServiceLabels(map[string]string{
|
||||
"com.docker.stack.namespace": "service-name-bar",
|
||||
}),
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
}, buf))
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "stack-list-sort.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
49
components/cli/cli/command/stack/opts_test.go
Normal file
49
components/cli/cli/command/stack/opts_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoadBundlefileErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
namespace string
|
||||
path string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
namespace: "namespace_foo",
|
||||
expectedError: fmt.Errorf("Bundle %s.dab not found", "namespace_foo"),
|
||||
},
|
||||
{
|
||||
namespace: "namespace_foo",
|
||||
path: "invalid_path",
|
||||
expectedError: fmt.Errorf("Bundle %s not found", "invalid_path"),
|
||||
},
|
||||
{
|
||||
namespace: "namespace_foo",
|
||||
path: filepath.Join("testdata", "bundlefile_with_invalid_syntax"),
|
||||
expectedError: fmt.Errorf("Error reading"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
_, err := loadBundlefile(&bytes.Buffer{}, tc.namespace, tc.path)
|
||||
assert.Error(t, err, tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBundlefile(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
namespace := ""
|
||||
path := filepath.Join("testdata", "bundlefile_with_two_services.dab")
|
||||
bundleFile, err := loadBundlefile(buf, namespace, path)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(bundleFile.Services), 2)
|
||||
}
|
||||
185
components/cli/cli/command/stack/ps_test.go
Normal file
185
components/cli/cli/command/stack/ps_test.go
Normal file
@ -0,0 +1,185 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
// Import builders to get the builder function as package function
|
||||
. "github.com/docker/cli/cli/internal/test/builders"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/pkg/testutil/golden"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStackPsErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error)
|
||||
expectedError string
|
||||
}{
|
||||
|
||||
{
|
||||
args: []string{},
|
||||
expectedError: "requires exactly 1 argument",
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
expectedError: "requires exactly 1 argument",
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
|
||||
return nil, errors.Errorf("error getting tasks")
|
||||
},
|
||||
expectedError: "error getting tasks",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cmd := newPsCommand(test.NewFakeCli(&fakeClient{
|
||||
taskListFunc: tc.taskListFunc,
|
||||
}, &bytes.Buffer{}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackPsEmptyStack(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newPsCommand(test.NewFakeCli(&fakeClient{
|
||||
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
|
||||
return []swarm.Task{}, nil
|
||||
},
|
||||
}, buf))
|
||||
cmd.SetArgs([]string{"foo"})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, buf.String(), "Nothing found in stack: foo")
|
||||
}
|
||||
|
||||
func TestStackPsWithQuietOption(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
|
||||
return []swarm.Task{*Task(TaskID("id-foo"))}, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newPsCommand(cli)
|
||||
cmd.SetArgs([]string{"foo"})
|
||||
cmd.Flags().Set("quiet", "true")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "stack-ps-with-quiet-option.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
|
||||
}
|
||||
|
||||
func TestStackPsWithNoTruncOption(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
|
||||
return []swarm.Task{*Task(TaskID("xn4cypcov06f2w8gsbaf2lst3"))}, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newPsCommand(cli)
|
||||
cmd.SetArgs([]string{"foo"})
|
||||
cmd.Flags().Set("no-trunc", "true")
|
||||
cmd.Flags().Set("format", "{{ .ID }}")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "stack-ps-with-no-trunc-option.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestStackPsWithNoResolveOption(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
|
||||
return []swarm.Task{*Task(
|
||||
TaskNodeID("id-node-foo"),
|
||||
)}, nil
|
||||
},
|
||||
nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) {
|
||||
return *Node(NodeName("node-name-bar")), nil, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newPsCommand(cli)
|
||||
cmd.SetArgs([]string{"foo"})
|
||||
cmd.Flags().Set("no-resolve", "true")
|
||||
cmd.Flags().Set("format", "{{ .Node }}")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "stack-ps-with-no-resolve-option.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestStackPsWithFormat(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
|
||||
return []swarm.Task{*Task(TaskServiceID("service-id-foo"))}, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newPsCommand(cli)
|
||||
cmd.SetArgs([]string{"foo"})
|
||||
cmd.Flags().Set("format", "{{ .Name }}")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "stack-ps-with-format.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestStackPsWithConfigFormat(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
|
||||
return []swarm.Task{*Task(TaskServiceID("service-id-foo"))}, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{
|
||||
TasksFormat: "{{ .Name }}",
|
||||
})
|
||||
cmd := newPsCommand(cli)
|
||||
cmd.SetArgs([]string{"foo"})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "stack-ps-with-config-format.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestStackPsWithoutFormat(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
|
||||
return []swarm.Task{*Task(
|
||||
TaskID("id-foo"),
|
||||
TaskServiceID("service-id-foo"),
|
||||
TaskNodeID("id-node"),
|
||||
WithTaskSpec(TaskImage("myimage:mytag")),
|
||||
TaskDesiredState(swarm.TaskStateReady),
|
||||
WithStatus(TaskState(swarm.TaskStateFailed), Timestamp(time.Now().Add(-2*time.Hour))),
|
||||
)}, nil
|
||||
},
|
||||
nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) {
|
||||
return *Node(NodeName("node-name-bar")), nil, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newPsCommand(cli)
|
||||
cmd.SetArgs([]string{"foo"})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "stack-ps-without-format.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
@ -55,10 +56,20 @@ func runRemove(dockerCli command.Cli, opts removeOptions) error {
|
||||
return err
|
||||
}
|
||||
|
||||
configs, err := getStackConfigs(ctx, client, namespace)
|
||||
var configs []swarm.Config
|
||||
|
||||
version, err := client.ServerVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if versions.LessThan(version.APIVersion, "1.30") {
|
||||
fmt.Fprintf(dockerCli.Err(), `WARNING: ignoring "configs" (requires API version 1.30, but the Docker daemon API version is %s)`, version.APIVersion)
|
||||
} else {
|
||||
configs, err = getStackConfigs(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
|
||||
fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace)
|
||||
|
||||
@ -3,6 +3,7 @@ package stack
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -86,7 +87,7 @@ func TestSkipEmptyStack(t *testing.T) {
|
||||
assert.Equal(t, allConfigIDs, cli.removedConfigs)
|
||||
}
|
||||
|
||||
func TestContinueAfterError(t *testing.T) {
|
||||
func TestRemoveContinueAfterError(t *testing.T) {
|
||||
allServices := []string{objectName("foo", "service1"), objectName("bar", "service1")}
|
||||
allServiceIDs := buildObjectIDs(allServices)
|
||||
|
||||
@ -116,6 +117,7 @@ func TestContinueAfterError(t *testing.T) {
|
||||
},
|
||||
}
|
||||
cmd := newRemoveCommand(test.NewFakeCli(cli, &bytes.Buffer{}))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs([]string{"foo", "bar"})
|
||||
|
||||
assert.EqualError(t, cmd.Execute(), "Failed to remove some resources from stack: foo")
|
||||
|
||||
@ -21,7 +21,7 @@ type servicesOptions struct {
|
||||
namespace string
|
||||
}
|
||||
|
||||
func newServicesCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
func newServicesCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options := servicesOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -41,7 +41,7 @@ func newServicesCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runServices(dockerCli *command.DockerCli, options servicesOptions) error {
|
||||
func runServices(dockerCli command.Cli, options servicesOptions) error {
|
||||
ctx := context.Background()
|
||||
client := dockerCli.Client()
|
||||
|
||||
|
||||
180
components/cli/cli/command/stack/services_test.go
Normal file
180
components/cli/cli/command/stack/services_test.go
Normal file
@ -0,0 +1,180 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
// Import builders to get the builder function as package function
|
||||
. "github.com/docker/cli/cli/internal/test/builders"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/pkg/testutil/golden"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStackServicesErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
flags map[string]string
|
||||
serviceListFunc func(options types.ServiceListOptions) ([]swarm.Service, error)
|
||||
nodeListFunc func(options types.NodeListOptions) ([]swarm.Node, error)
|
||||
taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"foo"},
|
||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return nil, errors.Errorf("error getting services")
|
||||
},
|
||||
expectedError: "error getting services",
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{*Service()}, nil
|
||||
},
|
||||
nodeListFunc: func(options types.NodeListOptions) ([]swarm.Node, error) {
|
||||
return nil, errors.Errorf("error getting nodes")
|
||||
},
|
||||
expectedError: "error getting nodes",
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{*Service()}, nil
|
||||
},
|
||||
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
|
||||
return nil, errors.Errorf("error getting tasks")
|
||||
},
|
||||
expectedError: "error getting tasks",
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
flags: map[string]string{
|
||||
"format": "{{invalid format}}",
|
||||
},
|
||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{*Service()}, nil
|
||||
},
|
||||
expectedError: "Template parsing error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
serviceListFunc: tc.serviceListFunc,
|
||||
nodeListFunc: tc.nodeListFunc,
|
||||
taskListFunc: tc.taskListFunc,
|
||||
}, &bytes.Buffer{})
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newServicesCommand(cli)
|
||||
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 TestStackServicesEmptyServiceList(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newServicesCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{}, nil
|
||||
},
|
||||
}, buf),
|
||||
)
|
||||
cmd.SetArgs([]string{"foo"})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, buf.String(), "Nothing found in stack: foo")
|
||||
}
|
||||
|
||||
func TestStackServicesWithQuietOption(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{*Service(ServiceID("id-foo"))}, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newServicesCommand(cli)
|
||||
cmd.Flags().Set("quiet", "true")
|
||||
cmd.SetArgs([]string{"foo"})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "stack-services-with-quiet-option.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestStackServicesWithFormat(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{
|
||||
*Service(ServiceName("service-name-foo")),
|
||||
}, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newServicesCommand(cli)
|
||||
cmd.SetArgs([]string{"foo"})
|
||||
cmd.Flags().Set("format", "{{ .Name }}")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "stack-services-with-format.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestStackServicesWithConfigFormat(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{
|
||||
*Service(ServiceName("service-name-foo")),
|
||||
}, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{
|
||||
ServicesFormat: "{{ .Name }}",
|
||||
})
|
||||
cmd := newServicesCommand(cli)
|
||||
cmd.SetArgs([]string{"foo"})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "stack-services-with-config-format.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestStackServicesWithoutFormat(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{*Service(
|
||||
ServiceName("name-foo"),
|
||||
ServiceID("id-foo"),
|
||||
ReplicatedService(2),
|
||||
ServiceImage("busybox:latest"),
|
||||
ServicePort(swarm.PortConfig{
|
||||
PublishMode: swarm.PortConfigPublishModeIngress,
|
||||
PublishedPort: 0,
|
||||
TargetPort: 3232,
|
||||
Protocol: swarm.PortConfigProtocolTCP,
|
||||
}),
|
||||
)}, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newServicesCommand(cli)
|
||||
cmd.SetArgs([]string{"foo"})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "stack-services-without-format.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
29
components/cli/cli/command/stack/testdata/bundlefile_with_two_services.dab
vendored
Normal file
29
components/cli/cli/command/stack/testdata/bundlefile_with_two_services.dab
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"Services": {
|
||||
"visualizer": {
|
||||
"Image": "busybox@sha256:32f093055929dbc23dec4d03e09dfe971f5973a9ca5cf059cbfb644c206aa83f",
|
||||
"Networks": [
|
||||
"webnet"
|
||||
],
|
||||
"Ports": [
|
||||
{
|
||||
"Port": 8080,
|
||||
"Protocol": "tcp"
|
||||
}
|
||||
]
|
||||
},
|
||||
"web": {
|
||||
"Image": "busybox@sha256:32f093055929dbc23dec4d03e09dfe971f5973a9ca5cf059cbfb644c206aa83f",
|
||||
"Networks": [
|
||||
"webnet"
|
||||
],
|
||||
"Ports": [
|
||||
{
|
||||
"Port": 80,
|
||||
"Protocol": "tcp"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Version": "0.1"
|
||||
}
|
||||
3
components/cli/cli/command/stack/testdata/stack-list-sort.golden
vendored
Normal file
3
components/cli/cli/command/stack/testdata/stack-list-sort.golden
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
NAME SERVICES
|
||||
service-name-bar 1
|
||||
service-name-foo 1
|
||||
1
components/cli/cli/command/stack/testdata/stack-list-with-format.golden
vendored
Normal file
1
components/cli/cli/command/stack/testdata/stack-list-with-format.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
service-name-foo
|
||||
2
components/cli/cli/command/stack/testdata/stack-list-without-format.golden
vendored
Normal file
2
components/cli/cli/command/stack/testdata/stack-list-without-format.golden
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
NAME SERVICES
|
||||
service-name-foo 1
|
||||
1
components/cli/cli/command/stack/testdata/stack-ps-with-config-format.golden
vendored
Normal file
1
components/cli/cli/command/stack/testdata/stack-ps-with-config-format.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
service-id-foo.1
|
||||
1
components/cli/cli/command/stack/testdata/stack-ps-with-format.golden
vendored
Normal file
1
components/cli/cli/command/stack/testdata/stack-ps-with-format.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
service-id-foo.1
|
||||
1
components/cli/cli/command/stack/testdata/stack-ps-with-no-resolve-option.golden
vendored
Normal file
1
components/cli/cli/command/stack/testdata/stack-ps-with-no-resolve-option.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
id-node-foo
|
||||
1
components/cli/cli/command/stack/testdata/stack-ps-with-no-trunc-option.golden
vendored
Normal file
1
components/cli/cli/command/stack/testdata/stack-ps-with-no-trunc-option.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
xn4cypcov06f2w8gsbaf2lst3
|
||||
1
components/cli/cli/command/stack/testdata/stack-ps-with-quiet-option.golden
vendored
Normal file
1
components/cli/cli/command/stack/testdata/stack-ps-with-quiet-option.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
id-foo
|
||||
2
components/cli/cli/command/stack/testdata/stack-ps-without-format.golden
vendored
Normal file
2
components/cli/cli/command/stack/testdata/stack-ps-without-format.golden
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
|
||||
id-foo service-id-foo.1 myimage:mytag node-name-bar Ready Failed 2 hours ago
|
||||
1
components/cli/cli/command/stack/testdata/stack-services-with-config-format.golden
vendored
Normal file
1
components/cli/cli/command/stack/testdata/stack-services-with-config-format.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
service-name-foo
|
||||
1
components/cli/cli/command/stack/testdata/stack-services-with-format.golden
vendored
Normal file
1
components/cli/cli/command/stack/testdata/stack-services-with-format.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
service-name-foo
|
||||
1
components/cli/cli/command/stack/testdata/stack-services-with-quiet-option.golden
vendored
Normal file
1
components/cli/cli/command/stack/testdata/stack-services-with-quiet-option.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
id-foo
|
||||
2
components/cli/cli/command/stack/testdata/stack-services-without-format.golden
vendored
Normal file
2
components/cli/cli/command/stack/testdata/stack-services-without-format.golden
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
ID NAME MODE REPLICAS IMAGE PORTS
|
||||
id-foo name-foo replicated 0/2 busybox:latest *:0->3232/tcp
|
||||
@ -92,7 +92,7 @@ func runInit(dockerCli command.Cli, flags *pflag.FlagSet, opts initOptions) erro
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not fetch unlock key")
|
||||
}
|
||||
printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey)
|
||||
printUnlockCommand(dockerCli.Out(), unlockKeyResp.UnlockKey)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@ -217,13 +217,13 @@ func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) {
|
||||
}
|
||||
|
||||
func addSwarmCAFlags(flags *pflag.FlagSet, opts *swarmOptions) {
|
||||
flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates (ns|us|ms|s|m|h)")
|
||||
flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, 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")
|
||||
}
|
||||
|
||||
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.dispatcherHeartbeat, flagDispatcherHeartbeat, 5*time.Second, "Dispatcher heartbeat period (ns|us|ms|s|m|h)")
|
||||
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")
|
||||
|
||||
@ -2,6 +2,7 @@ package swarm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
@ -74,16 +75,15 @@ func runUnlockKey(dockerCli command.Cli, opts unlockKeyOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey)
|
||||
printUnlockCommand(dockerCli.Out(), unlockKeyResp.UnlockKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
func printUnlockCommand(ctx context.Context, dockerCli command.Cli, unlockKey string) {
|
||||
func printUnlockCommand(out io.Writer, unlockKey string) {
|
||||
if len(unlockKey) > 0 {
|
||||
fmt.Fprintf(dockerCli.Out(), "To unlock a swarm manager after it restarts, "+
|
||||
fmt.Fprintf(out, "To unlock a swarm manager after it restarts, "+
|
||||
"run the `docker swarm unlock`\ncommand and provide the following key:\n\n %s\n\n"+
|
||||
"Please remember to store this key in a password manager, since without it you\n"+
|
||||
"will not be able to restart the manager.\n", unlockKey)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -19,7 +19,6 @@ 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
|
||||
|
||||
@ -65,7 +65,7 @@ func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, opts swarmOptions) e
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not fetch unlock key")
|
||||
}
|
||||
printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey)
|
||||
printUnlockCommand(dockerCli.Out(), unlockKeyResp.UnlockKey)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@ -5,7 +5,6 @@ import (
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
@ -120,7 +119,7 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error {
|
||||
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(), " Heartbeat Period: %s\n", units.HumanDuration(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))
|
||||
fmt.Fprintf(dockerCli.Out(), " Force Rotate: %d\n", info.Swarm.Cluster.Spec.CAConfig.ForceRotate)
|
||||
@ -264,7 +263,7 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error {
|
||||
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 {
|
||||
if !registry.Secure {
|
||||
fmt.Fprintf(dockerCli.Out(), " %s\n", registry.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,7 +68,7 @@ func inspectImages(ctx context.Context, dockerCli *command.DockerCli) inspect.Ge
|
||||
|
||||
func inspectNetwork(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc {
|
||||
return func(ref string) (interface{}, []byte, error) {
|
||||
return dockerCli.Client().NetworkInspectWithRaw(ctx, ref, false)
|
||||
return dockerCli.Client().NetworkInspectWithRaw(ctx, ref, types.NetworkInspectOptions{})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,6 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
volumetypes "github.com/docker/docker/api/types/volume"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
@ -57,7 +56,7 @@ func runCreate(dockerCli command.Cli, options createOptions) error {
|
||||
Driver: options.driver,
|
||||
DriverOpts: options.driverOpts.GetAll(),
|
||||
Name: options.name,
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
}
|
||||
|
||||
vol, err := client.VolumeCreate(context.Background(), volReq)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user