diff --git a/components/cli/.gitignore b/components/cli/.gitignore index 09d363d1ce..fa99439ac8 100644 --- a/components/cli/.gitignore +++ b/components/cli/.gitignore @@ -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 diff --git a/components/cli/Makefile b/components/cli/Makefile index b9180d4e02..dbb3d3bfef 100644 --- a/components/cli/Makefile +++ b/components/cli/Makefile @@ -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 diff --git a/components/cli/VERSION b/components/cli/VERSION index 2d736aaa18..3e9257aa26 100644 --- a/components/cli/VERSION +++ b/components/cli/VERSION @@ -1 +1 @@ -17.06.0-dev +17.07.0-dev diff --git a/components/cli/circle.yml b/components/cli/circle.yml index dabc4c5aad..2035a25f54 100644 --- a/components/cli/circle.yml +++ b/components/cli/circle.yml @@ -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 diff --git a/components/cli/cli/command/cli.go b/components/cli/cli/command/cli.go index 59efa9ac2a..a8529b512d 100644 --- a/components/cli/cli/command/cli.go +++ b/components/cli/cli/command/cli.go @@ -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 diff --git a/components/cli/cli/command/config/create.go b/components/cli/cli/command/config/create.go index 3f0a7db05e..437137c160 100644 --- a/components/cli/cli/command/config/create.go +++ b/components/cli/cli/command/config/create.go @@ -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, } diff --git a/components/cli/cli/command/config/remove_test.go b/components/cli/cli/command/config/remove_test.go index 84423c0254..c183a93c8b 100644 --- a/components/cli/cli/command/config/remove_test.go +++ b/components/cli/cli/command/config/remove_test.go @@ -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) } diff --git a/components/cli/cli/command/container/attach.go b/components/cli/cli/command/container/attach.go index e8abb1c748..dce6432846 100644 --- a/components/cli/cli/command/container/attach.go +++ b/components/cli/cli/command/container/attach.go @@ -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() diff --git a/components/cli/cli/command/container/attach_test.go b/components/cli/cli/command/container/attach_test.go new file mode 100644 index 0000000000..14b8137dae --- /dev/null +++ b/components/cli/cli/command/container/attach_test.go @@ -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) + } +} diff --git a/components/cli/cli/command/container/client_test.go b/components/cli/cli/command/container/client_test.go new file mode 100644 index 0000000000..1e2d5d9cf8 --- /dev/null +++ b/components/cli/cli/command/container/client_test.go @@ -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 +} diff --git a/components/cli/cli/command/container/exec.go b/components/cli/cli/command/container/exec.go index 587cc00f2f..bbcd34ae0c 100644 --- a/components/cli/cli/command/container/exec.go +++ b/components/cli/cli/command/container/exec.go @@ -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. diff --git a/components/cli/cli/command/container/exec_test.go b/components/cli/cli/command/container/exec_test.go index e94beb4545..b2df1c2b0c 100644 --- a/components/cli/cli/command/container/exec_test.go +++ b/components/cli/cli/command/container/exec_test.go @@ -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) + } +} diff --git a/components/cli/cli/command/container/hijack.go b/components/cli/cli/command/container/hijack.go index c6815a4d7d..b6d467d081 100644 --- a/components/cli/cli/command/container/hijack.go +++ b/components/cli/cli/command/container/hijack.go @@ -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) } } diff --git a/components/cli/cli/command/container/opts.go b/components/cli/cli/command/container/opts.go index b636733815..cf1f931b29 100644 --- a/components/cli/cli/command/container/opts.go +++ b/components/cli/cli/command/container/opts.go @@ -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) diff --git a/components/cli/cli/command/container/opts_test.go b/components/cli/cli/command/container/opts_test.go index e10754f31b..25e5b04fc8 100644 --- a/components/cli/cli/command/container/opts_test.go +++ b/components/cli/cli/command/container/opts_test.go @@ -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", diff --git a/components/cli/cli/command/container/run.go b/components/cli/cli/command/container/run.go index 722ad22e69..56f0d0d57b 100644 --- a/components/cli/cli/command/container/run.go +++ b/components/cli/cli/command/container/run.go @@ -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 diff --git a/components/cli/cli/command/container/start.go b/components/cli/cli/command/container/start.go index af8255de8b..6cd4751117 100644 --- a/components/cli/cli/command/container/start.go +++ b/components/cli/cli/command/container/start.go @@ -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 { diff --git a/components/cli/cli/command/container/stats.go b/components/cli/cli/command/container/stats.go index da6a6d059b..50495f00c3 100644 --- a/components/cli/cli/command/container/stats.go +++ b/components/cli/cli/command/container/stats.go @@ -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) diff --git a/components/cli/cli/command/container/stats_helpers.go b/components/cli/cli/command/container/stats_helpers.go index c6849c805a..eb12cd0dec 100644 --- a/components/cli/cli/command/container/stats_helpers.go +++ b/components/cli/cli/command/container/stats_helpers.go @@ -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 diff --git a/components/cli/cli/command/container/tty.go b/components/cli/cli/command/container/tty.go index 9b09d4a0f9..fe123eb108 100644 --- a/components/cli/cli/command/container/tty.go +++ b/components/cli/cli/command/container/tty.go @@ -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() { diff --git a/components/cli/cli/command/container/update.go b/components/cli/cli/command/container/update.go index c13c7e6ab1..9cb74aeb76 100644 --- a/components/cli/cli/command/container/update.go +++ b/components/cli/cli/command/container/update.go @@ -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 } diff --git a/components/cli/cli/command/container/utils.go b/components/cli/cli/command/container/utils.go index fbb38e8557..d9afe24163 100644 --- a/components/cli/cli/command/container/utils.go +++ b/components/cli/cli/command/container/utils.go @@ -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. diff --git a/components/cli/cli/command/formatter/container.go b/components/cli/cli/command/formatter/container.go index 9b5c24636c..5ee1309d28 100644 --- a/components/cli/cli/command/formatter/container.go +++ b/components/cli/cli/command/formatter/container.go @@ -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 +} diff --git a/components/cli/cli/command/formatter/container_test.go b/components/cli/cli/command/formatter/container_test.go index 5b1451c29e..0977a39c02 100644 --- a/components/cli/cli/command/formatter/container_test.go +++ b/components/cli/cli/command/formatter/container_test.go @@ -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) + } +} diff --git a/components/cli/cli/command/formatter/disk_usage.go b/components/cli/cli/command/formatter/disk_usage.go index 07e39826ed..9a3b4b9fe6 100644 --- a/components/cli/cli/command/formatter/disk_usage.go +++ b/components/cli/cli/command/formatter/disk_usage.go @@ -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 } diff --git a/components/cli/cli/command/formatter/history.go b/components/cli/cli/command/formatter/history.go index ed2af4637e..161aaa4b00 100644 --- a/components/cli/cli/command/formatter/history.go +++ b/components/cli/cli/command/formatter/history.go @@ -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 } diff --git a/components/cli/cli/command/formatter/image.go b/components/cli/cli/command/formatter/image.go index 3aae34ea11..cbd3edad3c 100644 --- a/components/cli/cli/command/formatter/image.go +++ b/components/cli/cli/command/formatter/image.go @@ -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 { diff --git a/components/cli/cli/command/formatter/network_test.go b/components/cli/cli/command/formatter/network_test.go index b8cab078e7..201838cf74 100644 --- a/components/cli/cli/command/formatter/network_test.go +++ b/components/cli/cli/command/formatter/network_test.go @@ -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) } } diff --git a/components/cli/cli/command/formatter/node_test.go b/components/cli/cli/command/formatter/node_test.go index 9ad25d10d5..42fdf5138d 100644 --- a/components/cli/cli/command/formatter/node_test.go +++ b/components/cli/cli/command/formatter/node_test.go @@ -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) } } diff --git a/components/cli/cli/command/formatter/reflect_test.go b/components/cli/cli/command/formatter/reflect_test.go index e547b18411..ffda51b858 100644 --- a/components/cli/cli/command/formatter/reflect_test.go +++ b/components/cli/cli/command/formatter/reflect_test.go @@ -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)" } diff --git a/components/cli/cli/command/formatter/service_test.go b/components/cli/cli/command/formatter/service_test.go index 629f853930..e230f80fbf 100644 --- a/components/cli/cli/command/formatter/service_test.go +++ b/components/cli/cli/command/formatter/service_test.go @@ -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) } } diff --git a/components/cli/cli/command/formatter/stats.go b/components/cli/cli/command/formatter/stats.go index c0151101a0..f53387e90a 100644 --- a/components/cli/cli/command/formatter/stats.go +++ b/components/cli/cli/command/formatter/stats.go @@ -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 diff --git a/components/cli/cli/command/formatter/volume_test.go b/components/cli/cli/command/formatter/volume_test.go index bf1100893f..f2c21a83c2 100644 --- a/components/cli/cli/command/formatter/volume_test.go +++ b/components/cli/cli/command/formatter/volume_test.go @@ -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) } } diff --git a/components/cli/cli/command/image/build.go b/components/cli/cli/command/image/build.go index da28e898e9..2077914717 100644 --- a/components/cli/cli/command/image/build.go +++ b/components/cli/cli/command/image/build.go @@ -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, diff --git a/components/cli/cli/command/image/build/context.go b/components/cli/cli/command/image/build/context.go index e6165aa975..b1457bdcc2 100644 --- a/components/cli/cli/command/image/build/context.go +++ b/components/cli/cli/command/image/build/context.go @@ -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 diff --git a/components/cli/cli/command/image/build/context_test.go b/components/cli/cli/command/image/build/context_test.go index cda0bfb3f1..8bcbea48dd 100644 --- a/components/cli/cli/command/image/build/context_test.go +++ b/components/cli/cli/command/image/build/context_test.go @@ -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) + } +} diff --git a/components/cli/cli/command/image/pull_test.go b/components/cli/cli/command/image/pull_test.go index b4b57e2abc..d72531b768 100644 --- a/components/cli/cli/command/image/pull_test.go +++ b/components/cli/cli/command/image/pull_test.go @@ -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", diff --git a/components/cli/cli/command/image/push_test.go b/components/cli/cli/command/image/push_test.go index b382ad7ee1..559b1b89c3 100644 --- a/components/cli/cli/command/image/push_test.go +++ b/components/cli/cli/command/image/push_test.go @@ -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", diff --git a/components/cli/cli/command/image/remove.go b/components/cli/cli/command/image/remove.go index 91bf2f8786..894d30574b 100644 --- a/components/cli/cli/command/image/remove.go +++ b/components/cli/cli/command/image/remove.go @@ -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 } diff --git a/components/cli/cli/command/image/remove_test.go b/components/cli/cli/command/image/remove_test.go index 0915729b20..9b02b24a90 100644 --- a/components/cli/cli/command/image/remove_test.go +++ b/components/cli/cli/command/image/remove_test.go @@ -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) diff --git a/components/cli/cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden b/components/cli/cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden index 4efc53719d..94db08448d 100644 --- a/components/cli/cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden +++ b/components/cli/cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden @@ -1,4 +1,2 @@ Untagged: image1 Deleted: image2 -Untagged: image1 -Deleted: image2 diff --git a/components/cli/cli/command/image/testdata/remove-command-success.Image Deleted with force option.golden b/components/cli/cli/command/image/testdata/remove-command-success.Image Deleted with force option.golden new file mode 100644 index 0000000000..e69de29bb2 diff --git a/components/cli/cli/command/image/testdata/remove-command-success.Image Deleted.golden b/components/cli/cli/command/image/testdata/remove-command-success.Image Deleted.golden index 382724d39f..445df11a97 100644 --- a/components/cli/cli/command/image/testdata/remove-command-success.Image Deleted.golden +++ b/components/cli/cli/command/image/testdata/remove-command-success.Image Deleted.golden @@ -1,2 +1 @@ Deleted: image1 -Deleted: image1 diff --git a/components/cli/cli/command/image/testdata/remove-command-success.Image Untagged.golden b/components/cli/cli/command/image/testdata/remove-command-success.Image Untagged.golden index c795dac19f..ebbb4075e8 100644 --- a/components/cli/cli/command/image/testdata/remove-command-success.Image Untagged.golden +++ b/components/cli/cli/command/image/testdata/remove-command-success.Image Untagged.golden @@ -1,2 +1 @@ Untagged: image1 -Untagged: image1 diff --git a/components/cli/cli/command/inspect/inspector.go b/components/cli/cli/command/inspect/inspector.go index 62a24ff369..054381f50f 100644 --- a/components/cli/cli/command/inspect/inspector.go +++ b/components/cli/cli/command/inspect/inspector.go @@ -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) diff --git a/components/cli/cli/command/inspect/inspector_test.go b/components/cli/cli/command/inspect/inspector_test.go index 9085230ac5..721bcf84c5 100644 --- a/components/cli/cli/command/inspect/inspector_test.go +++ b/components/cli/cli/command/inspect/inspector_test.go @@ -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() + } +} diff --git a/components/cli/cli/command/network/create.go b/components/cli/cli/command/network/create.go index 88f7a62611..ed6d2de7bd 100644 --- a/components/cli/cli/command/network/create.go +++ b/components/cli/cli/command/network/create.go @@ -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 != "" { diff --git a/components/cli/cli/command/network/inspect.go b/components/cli/cli/command/network/inspect.go index 0c76e87351..9856c04e4c 100644 --- a/components/cli/cli/command/network/inspect.go +++ b/components/cli/cli/command/network/inspect.go @@ -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) diff --git a/components/cli/cli/command/network/remove.go b/components/cli/cli/command/network/remove.go index c83016a408..7dfe8da2f7 100644 --- a/components/cli/cli/command/network/remove.go +++ b/components/cli/cli/command/network/remove.go @@ -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 diff --git a/components/cli/cli/command/node/opts.go b/components/cli/cli/command/node/opts.go index 484ab5edaf..e30e5de910 100644 --- a/components/cli/cli/command/node/opts.go +++ b/components/cli/cli/command/node/opts.go @@ -11,7 +11,6 @@ type nodeOptions struct { } type annotations struct { - name string labels opts.ListOpts } diff --git a/components/cli/cli/command/node/ps_test.go b/components/cli/cli/command/node/ps_test.go index ddc0eba0d4..d25a55f0cf 100644 --- a/components/cli/cli/command/node/ps_test.go +++ b/components/cli/cli/command/node/ps_test.go @@ -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 }, diff --git a/components/cli/cli/command/node/update.go b/components/cli/cli/command/node/update.go index 4ba5f6d38b..017cf7dcb5 100644 --- a/components/cli/cli/command/node/update.go +++ b/components/cli/cli/command/node/update.go @@ -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 } } diff --git a/components/cli/cli/command/registry/login.go b/components/cli/cli/command/registry/login.go index af79e967cc..ba1b133054 100644 --- a/components/cli/cli/command/registry/login.go +++ b/components/cli/cli/command/registry/login.go @@ -16,7 +16,6 @@ type loginOptions struct { serverAddress string user string password string - email string } // NewLoginCommand creates a new `docker login` command diff --git a/components/cli/cli/command/secret/create.go b/components/cli/cli/command/secret/create.go index e708f7c591..20efaec752 100644 --- a/components/cli/cli/command/secret/create.go +++ b/components/cli/cli/command/secret/create.go @@ -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, } diff --git a/components/cli/cli/command/secret/remove_test.go b/components/cli/cli/command/secret/remove_test.go index b6085c9890..08443ec9ae 100644 --- a/components/cli/cli/command/secret/remove_test.go +++ b/components/cli/cli/command/secret/remove_test.go @@ -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) diff --git a/components/cli/cli/command/service/helpers.go b/components/cli/cli/command/service/helpers.go index 2e893f96e1..8bc745dcef 100644 --- a/components/cli/cli/command/service/helpers.go +++ b/components/cli/cli/command/service/helpers.go @@ -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() diff --git a/components/cli/cli/command/service/inspect.go b/components/cli/cli/command/service/inspect.go index 45e7459dd4..ea0009d3a5 100644 --- a/components/cli/cli/command/service/inspect.go +++ b/components/cli/cli/command/service/inspect.go @@ -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 } diff --git a/components/cli/cli/command/service/inspect_test.go b/components/cli/cli/command/service/inspect_test.go index c2984a787a..e7b34da212 100644 --- a/components/cli/cli/command/service/inspect_test.go +++ b/components/cli/cli/command/service/inspect_test.go @@ -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) } diff --git a/components/cli/cli/command/service/logs.go b/components/cli/cli/command/service/logs.go index 42e00e6cd2..3d3b3f1d46 100644 --- a/components/cli/cli/command/service/logs.go +++ b/components/cli/cli/command/service/logs.go @@ -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 diff --git a/components/cli/cli/command/service/opts.go b/components/cli/cli/command/service/opts.go index c173f4f70b..4c1425c42c 100644 --- a/components/cli/cli/command/service/opts.go +++ b/components/cli/cli/command/service/opts.go @@ -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(), } } diff --git a/components/cli/cli/command/service/parse.go b/components/cli/cli/command/service/parse.go index f3c9306881..6f69cbb47f 100644 --- a/components/cli/cli/command/service/parse.go +++ b/components/cli/cli/command/service/parse.go @@ -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() diff --git a/components/cli/cli/command/service/progress/progress.go b/components/cli/cli/command/service/progress/progress.go index 88cc9e5fac..8e37489bdb 100644 --- a/components/cli/cli/command/service/progress/progress.go +++ b/components/cli/cli/command/service/progress/progress.go @@ -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 diff --git a/components/cli/cli/command/service/ps.go b/components/cli/cli/command/service/ps.go index 87b29d2043..741f6b589f 100644 --- a/components/cli/cli/command/service/ps.go +++ b/components/cli/cli/command/service/ps.go @@ -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 } diff --git a/components/cli/cli/command/service/ps_test.go b/components/cli/cli/command/service/ps_test.go new file mode 100644 index 0000000000..a53748d6d3 --- /dev/null +++ b/components/cli/cli/command/service/ps_test.go @@ -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") +} diff --git a/components/cli/cli/command/service/update.go b/components/cli/cli/command/service/update.go index 5e3bc7c458..a80bf01ef1 100644 --- a/components/cli/cli/command/service/update.go +++ b/components/cli/cli/command/service/update.go @@ -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 } diff --git a/components/cli/cli/command/stack/client_test.go b/components/cli/cli/command/stack/client_test.go index d4c373ce12..50442783fa 100644 --- a/components/cli/cli/command/stack/client_test.go +++ b/components/cli/cli/command/stack/client_test.go @@ -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) diff --git a/components/cli/cli/command/stack/deploy.go b/components/cli/cli/command/stack/deploy.go index d18a43484d..8e14b70d2f 100644 --- a/components/cli/cli/command/stack/deploy.go +++ b/components/cli/cli/command/stack/deploy.go @@ -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 diff --git a/components/cli/cli/command/stack/deploy_bundlefile.go b/components/cli/cli/command/stack/deploy_bundlefile.go index 2f2a9aa042..1074210e97 100644 --- a/components/cli/cli/command/stack/deploy_bundlefile.go +++ b/components/cli/cli/command/stack/deploy_bundlefile.go @@ -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) } diff --git a/components/cli/cli/command/stack/deploy_composefile.go b/components/cli/cli/command/stack/deploy_composefile.go index 642867cba9..8e5dfed4d5 100644 --- a/components/cli/cli/command/stack/deploy_composefile.go +++ b/components/cli/cli/command/stack/deploy_composefile.go @@ -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 } diff --git a/components/cli/cli/command/stack/deploy_composefile_test.go b/components/cli/cli/command/stack/deploy_composefile_test.go index 6d811ed208..5d4813bc05 100644 --- a/components/cli/cli/command/stack/deploy_composefile_test.go +++ b/components/cli/cli/command/stack/deploy_composefile_test.go @@ -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 }, } diff --git a/components/cli/cli/command/stack/list.go b/components/cli/cli/command/stack/list.go index 4560a98ec9..f3781d260c 100644 --- a/components/cli/cli/command/stack/list.go +++ b/components/cli/cli/command/stack/list.go @@ -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] diff --git a/components/cli/cli/command/stack/list_test.go b/components/cli/cli/command/stack/list_test.go new file mode 100644 index 0000000000..4f258977ed --- /dev/null +++ b/components/cli/cli/command/stack/list_test.go @@ -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)) +} diff --git a/components/cli/cli/command/stack/opts_test.go b/components/cli/cli/command/stack/opts_test.go new file mode 100644 index 0000000000..b57dcd89f6 --- /dev/null +++ b/components/cli/cli/command/stack/opts_test.go @@ -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) +} diff --git a/components/cli/cli/command/stack/ps_test.go b/components/cli/cli/command/stack/ps_test.go new file mode 100644 index 0000000000..afda419eb4 --- /dev/null +++ b/components/cli/cli/command/stack/ps_test.go @@ -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)) +} diff --git a/components/cli/cli/command/stack/remove.go b/components/cli/cli/command/stack/remove.go index 09431dad70..b63c385a7e 100644 --- a/components/cli/cli/command/stack/remove.go +++ b/components/cli/cli/command/stack/remove.go @@ -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) diff --git a/components/cli/cli/command/stack/remove_test.go b/components/cli/cli/command/stack/remove_test.go index d6efb90106..498416ebe8 100644 --- a/components/cli/cli/command/stack/remove_test.go +++ b/components/cli/cli/command/stack/remove_test.go @@ -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") diff --git a/components/cli/cli/command/stack/services.go b/components/cli/cli/command/stack/services.go index 459d7dd5a2..5b59c479c6 100644 --- a/components/cli/cli/command/stack/services.go +++ b/components/cli/cli/command/stack/services.go @@ -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() diff --git a/components/cli/cli/command/stack/services_test.go b/components/cli/cli/command/stack/services_test.go new file mode 100644 index 0000000000..a87174a14d --- /dev/null +++ b/components/cli/cli/command/stack/services_test.go @@ -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)) +} diff --git a/components/cli/cli/command/stack/testdata/bundlefile_with_two_services.dab b/components/cli/cli/command/stack/testdata/bundlefile_with_two_services.dab new file mode 100644 index 0000000000..ced8180dc0 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/bundlefile_with_two_services.dab @@ -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" +} diff --git a/components/cli/cli/command/stack/testdata/stack-list-sort.golden b/components/cli/cli/command/stack/testdata/stack-list-sort.golden new file mode 100644 index 0000000000..6bd116ebc6 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-list-sort.golden @@ -0,0 +1,3 @@ +NAME SERVICES +service-name-bar 1 +service-name-foo 1 diff --git a/components/cli/cli/command/stack/testdata/stack-list-with-format.golden b/components/cli/cli/command/stack/testdata/stack-list-with-format.golden new file mode 100644 index 0000000000..b53e6401f7 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-list-with-format.golden @@ -0,0 +1 @@ +service-name-foo diff --git a/components/cli/cli/command/stack/testdata/stack-list-without-format.golden b/components/cli/cli/command/stack/testdata/stack-list-without-format.golden new file mode 100644 index 0000000000..1b255654df --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-list-without-format.golden @@ -0,0 +1,2 @@ +NAME SERVICES +service-name-foo 1 diff --git a/components/cli/cli/command/stack/testdata/stack-ps-with-config-format.golden b/components/cli/cli/command/stack/testdata/stack-ps-with-config-format.golden new file mode 100644 index 0000000000..9ecebdafe3 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-ps-with-config-format.golden @@ -0,0 +1 @@ +service-id-foo.1 diff --git a/components/cli/cli/command/stack/testdata/stack-ps-with-format.golden b/components/cli/cli/command/stack/testdata/stack-ps-with-format.golden new file mode 100644 index 0000000000..9ecebdafe3 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-ps-with-format.golden @@ -0,0 +1 @@ +service-id-foo.1 diff --git a/components/cli/cli/command/stack/testdata/stack-ps-with-no-resolve-option.golden b/components/cli/cli/command/stack/testdata/stack-ps-with-no-resolve-option.golden new file mode 100644 index 0000000000..b90d743b8a --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-ps-with-no-resolve-option.golden @@ -0,0 +1 @@ +id-node-foo diff --git a/components/cli/cli/command/stack/testdata/stack-ps-with-no-trunc-option.golden b/components/cli/cli/command/stack/testdata/stack-ps-with-no-trunc-option.golden new file mode 100644 index 0000000000..8179bf4d65 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-ps-with-no-trunc-option.golden @@ -0,0 +1 @@ +xn4cypcov06f2w8gsbaf2lst3 diff --git a/components/cli/cli/command/stack/testdata/stack-ps-with-quiet-option.golden b/components/cli/cli/command/stack/testdata/stack-ps-with-quiet-option.golden new file mode 100644 index 0000000000..e2faeb6067 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-ps-with-quiet-option.golden @@ -0,0 +1 @@ +id-foo diff --git a/components/cli/cli/command/stack/testdata/stack-ps-without-format.golden b/components/cli/cli/command/stack/testdata/stack-ps-without-format.golden new file mode 100644 index 0000000000..9ca75f5890 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-ps-without-format.golden @@ -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 diff --git a/components/cli/cli/command/stack/testdata/stack-services-with-config-format.golden b/components/cli/cli/command/stack/testdata/stack-services-with-config-format.golden new file mode 100644 index 0000000000..b53e6401f7 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-services-with-config-format.golden @@ -0,0 +1 @@ +service-name-foo diff --git a/components/cli/cli/command/stack/testdata/stack-services-with-format.golden b/components/cli/cli/command/stack/testdata/stack-services-with-format.golden new file mode 100644 index 0000000000..b53e6401f7 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-services-with-format.golden @@ -0,0 +1 @@ +service-name-foo diff --git a/components/cli/cli/command/stack/testdata/stack-services-with-quiet-option.golden b/components/cli/cli/command/stack/testdata/stack-services-with-quiet-option.golden new file mode 100644 index 0000000000..e2faeb6067 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-services-with-quiet-option.golden @@ -0,0 +1 @@ +id-foo diff --git a/components/cli/cli/command/stack/testdata/stack-services-without-format.golden b/components/cli/cli/command/stack/testdata/stack-services-without-format.golden new file mode 100644 index 0000000000..c892250c20 --- /dev/null +++ b/components/cli/cli/command/stack/testdata/stack-services-without-format.golden @@ -0,0 +1,2 @@ +ID NAME MODE REPLICAS IMAGE PORTS +id-foo name-foo replicated 0/2 busybox:latest *:0->3232/tcp diff --git a/components/cli/cli/command/swarm/init.go b/components/cli/cli/command/swarm/init.go index ea3189a0c7..91b827eb97 100644 --- a/components/cli/cli/command/swarm/init.go +++ b/components/cli/cli/command/swarm/init.go @@ -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 diff --git a/components/cli/cli/command/swarm/opts.go b/components/cli/cli/command/swarm/opts.go index 4625835055..0522f9fdfa 100644 --- a/components/cli/cli/command/swarm/opts.go +++ b/components/cli/cli/command/swarm/opts.go @@ -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") diff --git a/components/cli/cli/command/swarm/unlock_key.go b/components/cli/cli/command/swarm/unlock_key.go index 870f8e4435..4618de7dea 100644 --- a/components/cli/cli/command/swarm/unlock_key.go +++ b/components/cli/cli/command/swarm/unlock_key.go @@ -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 } diff --git a/components/cli/cli/command/swarm/unlock_test.go b/components/cli/cli/command/swarm/unlock_test.go index 991365f873..3dae5239f5 100644 --- a/components/cli/cli/command/swarm/unlock_test.go +++ b/components/cli/cli/command/swarm/unlock_test.go @@ -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 diff --git a/components/cli/cli/command/swarm/update.go b/components/cli/cli/command/swarm/update.go index 561ca0a768..4d751bbd4e 100644 --- a/components/cli/cli/command/swarm/update.go +++ b/components/cli/cli/command/swarm/update.go @@ -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 diff --git a/components/cli/cli/command/system/info.go b/components/cli/cli/command/system/info.go index 7b91066974..f2a404e905 100644 --- a/components/cli/cli/command/system/info.go +++ b/components/cli/cli/command/system/info.go @@ -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) } } diff --git a/components/cli/cli/command/system/inspect.go b/components/cli/cli/command/system/inspect.go index cac6d0fb2f..6d079cd69e 100644 --- a/components/cli/cli/command/system/inspect.go +++ b/components/cli/cli/command/system/inspect.go @@ -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{}) } } diff --git a/components/cli/cli/command/volume/create.go b/components/cli/cli/command/volume/create.go index bf7f1a72b6..5a1092266f 100644 --- a/components/cli/cli/command/volume/create.go +++ b/components/cli/cli/command/volume/create.go @@ -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) diff --git a/components/cli/cli/compose/convert/service.go b/components/cli/cli/compose/convert/service.go index 233a376293..730f5e1ee5 100644 --- a/components/cli/cli/compose/convert/service.go +++ b/components/cli/cli/compose/convert/service.go @@ -14,11 +14,14 @@ 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/pkg/errors" ) -const defaultNetwork = "default" +const ( + defaultNetwork = "default" + // LabelImage is the label used to store image name provided in the compose file + LabelImage = "com.docker.stack.image" +) // Services from compose-file types to engine API types func Services( @@ -42,7 +45,7 @@ func Services( return nil, errors.Wrapf(err, "service %s", service.Name) } - serviceSpec, err := convertService(client.ClientVersion(), namespace, service, networks, volumes, secrets, configs) + serviceSpec, err := Service(client.ClientVersion(), namespace, service, networks, volumes, secrets, configs) if err != nil { return nil, errors.Wrapf(err, "service %s", service.Name) } @@ -52,7 +55,8 @@ func Services( return result, nil } -func convertService( +// Service converts a ServiceConfig into a swarm ServiceSpec +func Service( apiVersion string, namespace Namespace, service composetypes.ServiceConfig, @@ -158,6 +162,9 @@ func convertService( UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), } + // add an image label to serviceSpec + serviceSpec.Labels[LabelImage] = service.Image + // ServiceSpec.Networks is deprecated and should not have been used by // this package. It is possible to update TaskTemplate.Networks, but it // is not possible to update ServiceSpec.Networks. Unfortunately, we @@ -404,7 +411,7 @@ func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) { // TODO: log if restart is being ignored if source == nil { - policy, err := runconfigopts.ParseRestartPolicy(restart) + policy, err := opts.ParseRestartPolicy(restart) if err != nil { return nil, err } @@ -556,9 +563,6 @@ func convertCredentialSpec(spec composetypes.CredentialSpecConfig) (*swarm.Crede if spec.File != "" && spec.Registry != "" { return nil, errors.New("Invalid credential spec - must provide one of `File` or `Registry`") } - - return &swarm.CredentialSpec{ - File: spec.File, - Registry: spec.Registry, - }, nil + swarmCredSpec := swarm.CredentialSpec(spec) + return &swarmCredSpec, nil } diff --git a/components/cli/cli/compose/loader/loader.go b/components/cli/cli/compose/loader/loader.go index f12a7f0eea..b456e1824d 100644 --- a/components/cli/cli/compose/loader/loader.go +++ b/components/cli/cli/compose/loader/loader.go @@ -14,7 +14,6 @@ import ( "github.com/docker/cli/cli/compose/template" "github.com/docker/cli/cli/compose/types" "github.com/docker/cli/opts" - runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/go-connections/nat" units "github.com/docker/go-units" shellwords "github.com/mattn/go-shellwords" @@ -196,7 +195,7 @@ func transform(source map[string]interface{}, target interface{}) error { data := mapstructure.Metadata{} config := &mapstructure.DecoderConfig{ DecodeHook: mapstructure.ComposeDecodeHookFunc( - transformHook, + createTransformHook(), mapstructure.StringToTimeDurationHookFunc()), Result: target, Metadata: &data, @@ -208,46 +207,33 @@ func transform(source map[string]interface{}, target interface{}) error { return decoder.Decode(source) } -func transformHook( - source reflect.Type, - target reflect.Type, - data interface{}, -) (interface{}, error) { - switch target { - case reflect.TypeOf(types.External{}): - return transformExternal(data) - case reflect.TypeOf(types.HealthCheckTest{}): - return transformHealthCheckTest(data) - case reflect.TypeOf(types.ShellCommand{}): - return transformShellCommand(data) - case reflect.TypeOf(types.StringList{}): - return transformStringList(data) - case reflect.TypeOf(map[string]string{}): - return transformMapStringString(data) - case reflect.TypeOf(types.UlimitsConfig{}): - return transformUlimits(data) - case reflect.TypeOf(types.UnitBytes(0)): - return transformSize(data) - case reflect.TypeOf([]types.ServicePortConfig{}): - return transformServicePort(data) - case reflect.TypeOf(types.ServiceSecretConfig{}): - return transformStringSourceMap(data) - case reflect.TypeOf(types.ServiceConfigObjConfig{}): - return transformStringSourceMap(data) - case reflect.TypeOf(types.StringOrNumberList{}): - return transformStringOrNumberList(data) - case reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}): - return transformServiceNetworkMap(data) - case reflect.TypeOf(types.MappingWithEquals{}): - return transformMappingOrList(data, "=", true), nil - case reflect.TypeOf(types.Labels{}): - return transformMappingOrList(data, "=", false), nil - case reflect.TypeOf(types.MappingWithColon{}): - return transformMappingOrList(data, ":", false), nil - case reflect.TypeOf(types.ServiceVolumeConfig{}): - return transformServiceVolumeConfig(data) +func createTransformHook() mapstructure.DecodeHookFuncType { + transforms := map[reflect.Type]func(interface{}) (interface{}, error){ + reflect.TypeOf(types.External{}): transformExternal, + reflect.TypeOf(types.HealthCheckTest{}): transformHealthCheckTest, + reflect.TypeOf(types.ShellCommand{}): transformShellCommand, + reflect.TypeOf(types.StringList{}): transformStringList, + reflect.TypeOf(map[string]string{}): transformMapStringString, + reflect.TypeOf(types.UlimitsConfig{}): transformUlimits, + reflect.TypeOf(types.UnitBytes(0)): transformSize, + reflect.TypeOf([]types.ServicePortConfig{}): transformServicePort, + reflect.TypeOf(types.ServiceSecretConfig{}): transformStringSourceMap, + reflect.TypeOf(types.ServiceConfigObjConfig{}): transformStringSourceMap, + reflect.TypeOf(types.StringOrNumberList{}): transformStringOrNumberList, + reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}): transformServiceNetworkMap, + reflect.TypeOf(types.MappingWithEquals{}): transformMappingOrListFunc("=", true), + reflect.TypeOf(types.Labels{}): transformMappingOrListFunc("=", false), + reflect.TypeOf(types.MappingWithColon{}): transformMappingOrListFunc(":", false), + reflect.TypeOf(types.ServiceVolumeConfig{}): transformServiceVolumeConfig, + } + + return func(_ reflect.Type, target reflect.Type, data interface{}) (interface{}, error) { + transform, ok := transforms[target] + if !ok { + return data, nil + } + return transform(data) } - return data, nil } // keys needs to be converted to strings for jsonschema @@ -351,14 +337,14 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, l for _, file := range serviceConfig.EnvFile { filePath := absPath(workingDir, file) - fileVars, err := runconfigopts.ParseEnvFile(filePath) + fileVars, err := opts.ParseEnvFile(filePath) if err != nil { return err } envVars = append(envVars, fileVars...) } updateEnvironment(environment, - runconfigopts.ConvertKVStringsToMapWithNil(envVars), lookupEnv) + opts.ConvertKVStringsToMapWithNil(envVars), lookupEnv) } updateEnvironment(environment, serviceConfig.Environment, lookupEnv) @@ -578,7 +564,7 @@ func transformStringSourceMap(data interface{}) (interface{}, error) { func transformServiceVolumeConfig(data interface{}) (interface{}, error) { switch value := data.(type) { case string: - return parseVolume(value) + return ParseVolume(value) case map[string]interface{}: return data, nil default: @@ -618,6 +604,12 @@ func transformStringList(data interface{}) (interface{}, error) { } } +func transformMappingOrListFunc(sep string, allowNil bool) func(interface{}) (interface{}, error) { + return func(data interface{}) (interface{}, error) { + return transformMappingOrList(data, sep, allowNil), nil + } +} + func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) interface{} { switch value := mappingOrList.(type) { case map[string]interface{}: @@ -659,7 +651,7 @@ func transformHealthCheckTest(data interface{}) (interface{}, error) { } } -func transformSize(value interface{}) (int64, error) { +func transformSize(value interface{}) (interface{}, error) { switch value := value.(type) { case int: return int64(value), nil diff --git a/components/cli/cli/compose/loader/loader_test.go b/components/cli/cli/compose/loader/loader_test.go index 4e604431d3..43a02204b7 100644 --- a/components/cli/cli/compose/loader/loader_test.go +++ b/components/cli/cli/compose/loader/loader_test.go @@ -632,10 +632,6 @@ func durationPtr(value time.Duration) *time.Duration { return &value } -func int64Ptr(value int64) *int64 { - return &value -} - func uint64Ptr(value uint64) *uint64 { return &value } diff --git a/components/cli/cli/compose/loader/volume.go b/components/cli/cli/compose/loader/volume.go index 0b8bc1b063..b026cf150f 100644 --- a/components/cli/cli/compose/loader/volume.go +++ b/components/cli/cli/compose/loader/volume.go @@ -10,7 +10,10 @@ import ( "github.com/pkg/errors" ) -func parseVolume(spec string) (types.ServiceVolumeConfig, error) { +const endOfSpec = rune(0) + +// ParseVolume parses a volume spec without any knowledge of the target platform +func ParseVolume(spec string) (types.ServiceVolumeConfig, error) { volume := types.ServiceVolumeConfig{} switch len(spec) { @@ -23,12 +26,13 @@ func parseVolume(spec string) (types.ServiceVolumeConfig, error) { } buffer := []rune{} - for _, char := range spec { + for _, char := range spec + string(endOfSpec) { switch { - case isWindowsDrive(char, buffer, volume): + case isWindowsDrive(buffer, char): buffer = append(buffer, char) - case char == ':': + case char == ':' || char == endOfSpec: if err := populateFieldFromBuffer(char, buffer, &volume); err != nil { + populateType(&volume) return volume, errors.Wrapf(err, "invalid spec: %s", spec) } buffer = []rune{} @@ -37,14 +41,11 @@ func parseVolume(spec string) (types.ServiceVolumeConfig, error) { } } - if err := populateFieldFromBuffer(rune(0), buffer, &volume); err != nil { - return volume, errors.Wrapf(err, "invalid spec: %s", spec) - } populateType(&volume) return volume, nil } -func isWindowsDrive(char rune, buffer []rune, volume types.ServiceVolumeConfig) bool { +func isWindowsDrive(buffer []rune, char rune) bool { return char == ':' && len(buffer) == 1 && unicode.IsLetter(buffer[0]) } @@ -54,7 +55,7 @@ func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolu case len(buffer) == 0: return errors.New("empty section between colons") // Anonymous volume - case volume.Source == "" && char == rune(0): + case volume.Source == "" && char == endOfSpec: volume.Target = strBuffer return nil case volume.Source == "": @@ -77,9 +78,8 @@ func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolu default: if isBindOption(option) { volume.Bind = &types.ServiceVolumeBind{Propagation: option} - } else { - return errors.Errorf("unknown option: %s", option) } + // ignore unknown options } } return nil @@ -112,10 +112,6 @@ func isFilePath(source string) bool { return true } - // Windows absolute path - first, next := utf8.DecodeRuneInString(source) - if unicode.IsLetter(first) && source[next] == ':' { - return true - } - return false + first, nextIndex := utf8.DecodeRuneInString(source) + return isWindowsDrive([]rune{first}, rune(source[nextIndex])) } diff --git a/components/cli/cli/compose/loader/volume_test.go b/components/cli/cli/compose/loader/volume_test.go index 79f1667731..f1b90fe8ea 100644 --- a/components/cli/cli/compose/loader/volume_test.go +++ b/components/cli/cli/compose/loader/volume_test.go @@ -1,6 +1,7 @@ package loader import ( + "fmt" "testing" "github.com/docker/cli/cli/compose/types" @@ -10,7 +11,7 @@ import ( func TestParseVolumeAnonymousVolume(t *testing.T) { for _, path := range []string{"/path", "/path/foo"} { - volume, err := parseVolume(path) + volume, err := ParseVolume(path) expected := types.ServiceVolumeConfig{Type: "volume", Target: path} assert.NoError(t, err) assert.Equal(t, expected, volume) @@ -19,7 +20,7 @@ func TestParseVolumeAnonymousVolume(t *testing.T) { func TestParseVolumeAnonymousVolumeWindows(t *testing.T) { for _, path := range []string{"C:\\path", "Z:\\path\\foo"} { - volume, err := parseVolume(path) + volume, err := ParseVolume(path) expected := types.ServiceVolumeConfig{Type: "volume", Target: path} assert.NoError(t, err) assert.Equal(t, expected, volume) @@ -27,13 +28,13 @@ func TestParseVolumeAnonymousVolumeWindows(t *testing.T) { } func TestParseVolumeTooManyColons(t *testing.T) { - _, err := parseVolume("/foo:/foo:ro:foo") + _, err := ParseVolume("/foo:/foo:ro:foo") assert.EqualError(t, err, "invalid spec: /foo:/foo:ro:foo: too many colons") } func TestParseVolumeShortVolumes(t *testing.T) { for _, path := range []string{".", "/a"} { - volume, err := parseVolume(path) + volume, err := ParseVolume(path) expected := types.ServiceVolumeConfig{Type: "volume", Target: path} assert.NoError(t, err) assert.Equal(t, expected, volume) @@ -42,14 +43,14 @@ func TestParseVolumeShortVolumes(t *testing.T) { func TestParseVolumeMissingSource(t *testing.T) { for _, spec := range []string{":foo", "/foo::ro"} { - _, err := parseVolume(spec) + _, err := ParseVolume(spec) testutil.ErrorContains(t, err, "empty section between colons") } } func TestParseVolumeBindMount(t *testing.T) { for _, path := range []string{"./foo", "~/thing", "../other", "/foo", "/home/user"} { - volume, err := parseVolume(path + ":/target") + volume, err := ParseVolume(path + ":/target") expected := types.ServiceVolumeConfig{ Type: "bind", Source: path, @@ -67,7 +68,7 @@ func TestParseVolumeRelativeBindMountWindows(t *testing.T) { "../other", "D:\\path", "/home/user", } { - volume, err := parseVolume(path + ":d:\\target") + volume, err := ParseVolume(path + ":d:\\target") expected := types.ServiceVolumeConfig{ Type: "bind", Source: path, @@ -79,7 +80,7 @@ func TestParseVolumeRelativeBindMountWindows(t *testing.T) { } func TestParseVolumeWithBindOptions(t *testing.T) { - volume, err := parseVolume("/source:/target:slave") + volume, err := ParseVolume("/source:/target:slave") expected := types.ServiceVolumeConfig{ Type: "bind", Source: "/source", @@ -91,7 +92,7 @@ func TestParseVolumeWithBindOptions(t *testing.T) { } func TestParseVolumeWithBindOptionsWindows(t *testing.T) { - volume, err := parseVolume("C:\\source\\foo:D:\\target:ro,rprivate") + volume, err := ParseVolume("C:\\source\\foo:D:\\target:ro,rprivate") expected := types.ServiceVolumeConfig{ Type: "bind", Source: "C:\\source\\foo", @@ -104,12 +105,12 @@ func TestParseVolumeWithBindOptionsWindows(t *testing.T) { } func TestParseVolumeWithInvalidVolumeOptions(t *testing.T) { - _, err := parseVolume("name:/target:bogus") - assert.EqualError(t, err, "invalid spec: name:/target:bogus: unknown option: bogus") + _, err := ParseVolume("name:/target:bogus") + assert.NoError(t, err) } func TestParseVolumeWithVolumeOptions(t *testing.T) { - volume, err := parseVolume("name:/target:nocopy") + volume, err := ParseVolume("name:/target:nocopy") expected := types.ServiceVolumeConfig{ Type: "volume", Source: "name", @@ -122,7 +123,7 @@ func TestParseVolumeWithVolumeOptions(t *testing.T) { func TestParseVolumeWithReadOnly(t *testing.T) { for _, path := range []string{"./foo", "/home/user"} { - volume, err := parseVolume(path + ":/target:ro") + volume, err := ParseVolume(path + ":/target:ro") expected := types.ServiceVolumeConfig{ Type: "bind", Source: path, @@ -136,7 +137,7 @@ func TestParseVolumeWithReadOnly(t *testing.T) { func TestParseVolumeWithRW(t *testing.T) { for _, path := range []string{"./foo", "/home/user"} { - volume, err := parseVolume(path + ":/target:rw") + volume, err := ParseVolume(path + ":/target:rw") expected := types.ServiceVolumeConfig{ Type: "bind", Source: path, @@ -147,3 +148,55 @@ func TestParseVolumeWithRW(t *testing.T) { assert.Equal(t, expected, volume) } } + +func TestIsFilePath(t *testing.T) { + assert.False(t, isFilePath("a界")) +} + +// Preserve the test cases for VolumeSplitN +func TestParseVolumeSplitCases(t *testing.T) { + for casenumber, 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`}}, + } { + parsed, _ := ParseVolume(x.input) + + expected := len(x.expected) > 1 + msg := fmt.Sprintf("Case %d: %s", casenumber, x.input) + assert.Equal(t, expected, parsed.Source != "", msg) + } +} diff --git a/components/cli/cli/config/configfile/file.go b/components/cli/cli/config/configfile/file.go index 7214325d87..9c2c4eec6d 100644 --- a/components/cli/cli/config/configfile/file.go +++ b/components/cli/cli/config/configfile/file.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" + "github.com/docker/cli/opts" "github.com/docker/docker/api/types" "github.com/pkg/errors" ) @@ -41,6 +42,15 @@ type ConfigFile struct { ConfigFormat string `json:"configFormat,omitempty"` NodesFormat string `json:"nodesFormat,omitempty"` PruneFilters []string `json:"pruneFilters,omitempty"` + Proxies map[string]ProxyConfig `json:"proxies,omitempty"` +} + +// ProxyConfig contains proxy configuration settings +type ProxyConfig struct { + HTTPProxy string `json:"httpProxy,omitempty"` + HTTPSProxy string `json:"httpsProxy,omitempty"` + NoProxy string `json:"noProxy,omitempty"` + FTPProxy string `json:"ftpProxy,omitempty"` } // LegacyLoadFromReader reads the non-nested configuration data given and sets up the @@ -152,6 +162,39 @@ func (configFile *ConfigFile) Save() error { return configFile.SaveToWriter(f) } +// ParseProxyConfig computes proxy configuration by retreiving the config for the provided host and +// then checking this against any environment variables provided to the container +func (configFile *ConfigFile) ParseProxyConfig(host string, runOpts []string) map[string]*string { + var cfgKey string + + if _, ok := configFile.Proxies[host]; !ok { + cfgKey = "default" + } else { + cfgKey = host + } + + config := configFile.Proxies[cfgKey] + permitted := map[string]*string{ + "HTTP_PROXY": &config.HTTPProxy, + "HTTPS_PROXY": &config.HTTPSProxy, + "NO_PROXY": &config.NoProxy, + "FTP_PROXY": &config.FTPProxy, + } + m := opts.ConvertKVStringsToMapWithNil(runOpts) + for k := range permitted { + if *permitted[k] == "" { + continue + } + if _, ok := m[k]; !ok { + m[k] = permitted[k] + } + if _, ok := m[strings.ToLower(k)]; !ok { + m[strings.ToLower(k)] = permitted[k] + } + } + return m +} + // encodeAuth creates a base64 encoded string to containing authorization information func encodeAuth(authConfig *types.AuthConfig) string { if authConfig.Username == "" && authConfig.Password == "" { diff --git a/components/cli/cli/config/configfile/file_test.go b/components/cli/cli/config/configfile/file_test.go index 435797f681..8c84347719 100644 --- a/components/cli/cli/config/configfile/file_test.go +++ b/components/cli/cli/config/configfile/file_test.go @@ -1,9 +1,11 @@ package configfile import ( + "fmt" "testing" "github.com/docker/docker/api/types" + "github.com/stretchr/testify/assert" ) func TestEncodeAuth(t *testing.T) { @@ -25,3 +27,118 @@ func TestEncodeAuth(t *testing.T) { t.Fatal("AuthString encoding isn't correct.") } } + +func TestProxyConfig(t *testing.T) { + httpProxy := "http://proxy.mycorp.com:3128" + httpsProxy := "https://user:password@proxy.mycorp.com:3129" + ftpProxy := "http://ftpproxy.mycorp.com:21" + noProxy := "*.intra.mycorp.com" + defaultProxyConfig := ProxyConfig{ + HTTPProxy: httpProxy, + HTTPSProxy: httpsProxy, + FTPProxy: ftpProxy, + NoProxy: noProxy, + } + + cfg := ConfigFile{ + Proxies: map[string]ProxyConfig{ + "default": defaultProxyConfig, + }, + } + + proxyConfig := cfg.ParseProxyConfig("/var/run/docker.sock", []string{}) + expected := map[string]*string{ + "HTTP_PROXY": &httpProxy, + "http_proxy": &httpProxy, + "HTTPS_PROXY": &httpsProxy, + "https_proxy": &httpsProxy, + "FTP_PROXY": &ftpProxy, + "ftp_proxy": &ftpProxy, + "NO_PROXY": &noProxy, + "no_proxy": &noProxy, + } + assert.Equal(t, expected, proxyConfig) +} + +func TestProxyConfigOverride(t *testing.T) { + httpProxy := "http://proxy.mycorp.com:3128" + overrideHTTPProxy := "http://proxy.example.com:3128" + overrideNoProxy := "" + httpsProxy := "https://user:password@proxy.mycorp.com:3129" + ftpProxy := "http://ftpproxy.mycorp.com:21" + noProxy := "*.intra.mycorp.com" + defaultProxyConfig := ProxyConfig{ + HTTPProxy: httpProxy, + HTTPSProxy: httpsProxy, + FTPProxy: ftpProxy, + NoProxy: noProxy, + } + + cfg := ConfigFile{ + Proxies: map[string]ProxyConfig{ + "default": defaultProxyConfig, + }, + } + + ropts := []string{ + fmt.Sprintf("HTTP_PROXY=%s", overrideHTTPProxy), + "NO_PROXY=", + } + proxyConfig := cfg.ParseProxyConfig("/var/run/docker.sock", ropts) + expected := map[string]*string{ + "HTTP_PROXY": &overrideHTTPProxy, + "http_proxy": &httpProxy, + "HTTPS_PROXY": &httpsProxy, + "https_proxy": &httpsProxy, + "FTP_PROXY": &ftpProxy, + "ftp_proxy": &ftpProxy, + "NO_PROXY": &overrideNoProxy, + "no_proxy": &noProxy, + } + assert.Equal(t, expected, proxyConfig) +} + +func TestProxyConfigPerHost(t *testing.T) { + httpProxy := "http://proxy.mycorp.com:3128" + httpsProxy := "https://user:password@proxy.mycorp.com:3129" + ftpProxy := "http://ftpproxy.mycorp.com:21" + noProxy := "*.intra.mycorp.com" + + extHTTPProxy := "http://proxy.example.com:3128" + extHTTPSProxy := "https://user:password@proxy.example.com:3129" + extFTPProxy := "http://ftpproxy.example.com:21" + extNoProxy := "*.intra.example.com" + + defaultProxyConfig := ProxyConfig{ + HTTPProxy: httpProxy, + HTTPSProxy: httpsProxy, + FTPProxy: ftpProxy, + NoProxy: noProxy, + } + externalProxyConfig := ProxyConfig{ + HTTPProxy: extHTTPProxy, + HTTPSProxy: extHTTPSProxy, + FTPProxy: extFTPProxy, + NoProxy: extNoProxy, + } + + cfg := ConfigFile{ + Proxies: map[string]ProxyConfig{ + "default": defaultProxyConfig, + "tcp://example.docker.com:2376": externalProxyConfig, + }, + } + + proxyConfig := cfg.ParseProxyConfig("tcp://example.docker.com:2376", []string{}) + expected := map[string]*string{ + "HTTP_PROXY": &extHTTPProxy, + "http_proxy": &extHTTPProxy, + "HTTPS_PROXY": &extHTTPSProxy, + "https_proxy": &extHTTPSProxy, + "FTP_PROXY": &extFTPProxy, + "ftp_proxy": &extFTPProxy, + "NO_PROXY": &extNoProxy, + "no_proxy": &extNoProxy, + } + assert.Equal(t, expected, proxyConfig) +} diff --git a/components/cli/cli/config/credentials/native_store.go b/components/cli/cli/config/credentials/native_store.go index 15bb0e912f..cef34db92f 100644 --- a/components/cli/cli/config/credentials/native_store.go +++ b/components/cli/cli/config/credentials/native_store.go @@ -73,7 +73,7 @@ func (c *nativeStore) GetAll() (map[string]types.AuthConfig, error) { if err != nil { return nil, err } - ac, _ := fileConfigs[registry] // might contain Email + ac := fileConfigs[registry] // might contain Email ac.Username = creds.Username ac.Password = creds.Password ac.IdentityToken = creds.IdentityToken diff --git a/components/cli/cli/internal/test/builders/service.go b/components/cli/cli/internal/test/builders/service.go index dee91f4baa..71718268e1 100644 --- a/components/cli/cli/internal/test/builders/service.go +++ b/components/cli/cli/internal/test/builders/service.go @@ -14,6 +14,7 @@ func Service(builders ...func(*swarm.Service)) *swarm.Service { Annotations: swarm.Annotations{ Name: "defaultServiceName", }, + EndpointSpec: &swarm.EndpointSpec{}, }, } @@ -24,9 +25,44 @@ func Service(builders ...func(*swarm.Service)) *swarm.Service { return service } +// ServiceID sets the service ID +func ServiceID(ID string) func(*swarm.Service) { + return func(service *swarm.Service) { + service.ID = ID + } +} + // ServiceName sets the service name func ServiceName(name string) func(*swarm.Service) { return func(service *swarm.Service) { service.Spec.Annotations.Name = name } } + +// ServiceLabels sets the service's labels +func ServiceLabels(labels map[string]string) func(*swarm.Service) { + return func(service *swarm.Service) { + service.Spec.Annotations.Labels = labels + } +} + +// ReplicatedService sets the number of replicas for the service +func ReplicatedService(replicas uint64) func(*swarm.Service) { + return func(service *swarm.Service) { + service.Spec.Mode = swarm.ServiceMode{Replicated: &swarm.ReplicatedService{Replicas: &replicas}} + } +} + +// ServiceImage sets the service's image +func ServiceImage(image string) func(*swarm.Service) { + return func(service *swarm.Service) { + service.Spec.TaskTemplate = swarm.TaskSpec{ContainerSpec: swarm.ContainerSpec{Image: image}} + } +} + +// ServicePort sets the service's port +func ServicePort(port swarm.PortConfig) func(*swarm.Service) { + return func(service *swarm.Service) { + service.Spec.EndpointSpec.Ports = append(service.Spec.EndpointSpec.Ports, port) + } +} diff --git a/components/cli/cli/internal/test/builders/task.go b/components/cli/cli/internal/test/builders/task.go index 688c62a3a8..b4551c983b 100644 --- a/components/cli/cli/internal/test/builders/task.go +++ b/components/cli/cli/internal/test/builders/task.go @@ -42,13 +42,34 @@ func TaskID(id string) func(*swarm.Task) { } } -// ServiceID sets the task service's ID -func ServiceID(id string) func(*swarm.Task) { +// TaskName sets the task name +func TaskName(name string) func(*swarm.Task) { + return func(task *swarm.Task) { + task.Annotations.Name = name + } +} + +// TaskServiceID sets the task service's ID +func TaskServiceID(id string) func(*swarm.Task) { return func(task *swarm.Task) { task.ServiceID = id } } +// TaskNodeID sets the task's node id +func TaskNodeID(id string) func(*swarm.Task) { + return func(task *swarm.Task) { + task.NodeID = id + } +} + +// TaskDesiredState sets the task's desired state +func TaskDesiredState(state swarm.TaskState) func(*swarm.Task) { + return func(task *swarm.Task) { + task.DesiredState = state + } +} + // WithStatus sets the task status func WithStatus(statusBuilders ...func(*swarm.TaskStatus)) func(*swarm.Task) { return func(task *swarm.Task) { @@ -86,6 +107,13 @@ func StatusErr(err string) func(*swarm.TaskStatus) { } } +// TaskState sets the task's current state +func TaskState(state swarm.TaskState) func(*swarm.TaskStatus) { + return func(taskStatus *swarm.TaskStatus) { + taskStatus.State = state + } +} + // PortStatus sets the tasks port config status // FIXME(vdemeester) should be a sub builder 👼 func PortStatus(portConfigs []swarm.PortConfig) func(*swarm.TaskStatus) { @@ -94,6 +122,13 @@ func PortStatus(portConfigs []swarm.PortConfig) func(*swarm.TaskStatus) { } } +// WithTaskSpec sets the task spec +func WithTaskSpec(specBuilders ...func(*swarm.TaskSpec)) func(*swarm.Task) { + return func(task *swarm.Task) { + task.Spec = *TaskSpec(specBuilders...) + } +} + // TaskSpec creates a task spec with default values . // Any number of taskSpec function builder can be pass to augment it. func TaskSpec(specBuilders ...func(*swarm.TaskSpec)) *swarm.TaskSpec { @@ -109,3 +144,10 @@ func TaskSpec(specBuilders ...func(*swarm.TaskSpec)) *swarm.TaskSpec { return taskSpec } + +// TaskImage sets the task's image +func TaskImage(image string) func(*swarm.TaskSpec) { + return func(taskSpec *swarm.TaskSpec) { + taskSpec.ContainerSpec.Image = image + } +} diff --git a/components/cli/cli/internal/test/network/client.go b/components/cli/cli/internal/test/network/client.go index 5f35cd4514..d83288d619 100644 --- a/components/cli/cli/internal/test/network/client.go +++ b/components/cli/cli/internal/test/network/client.go @@ -9,7 +9,7 @@ import ( // FakeClient is a fake NetworkAPIClient type FakeClient struct { - NetworkInspectFunc func(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, error) + NetworkInspectFunc func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) } // NetworkConnect fakes connecting to a network @@ -18,30 +18,30 @@ func (c *FakeClient) NetworkConnect(ctx context.Context, networkID, container st } // NetworkCreate fakes creating a network -func (c *FakeClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) { +func (c *FakeClient) NetworkCreate(_ context.Context, _ string, options types.NetworkCreate) (types.NetworkCreateResponse, error) { return types.NetworkCreateResponse{}, nil } -// NetworkDisconnect fakes disconencting from a network +// NetworkDisconnect fakes disconnecting from a network func (c *FakeClient) NetworkDisconnect(ctx context.Context, networkID, container string, force bool) error { return nil } // NetworkInspect fakes inspecting a network -func (c *FakeClient) NetworkInspect(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, error) { +func (c *FakeClient) NetworkInspect(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) { if c.NetworkInspectFunc != nil { - return c.NetworkInspectFunc(ctx, networkID, verbose) + return c.NetworkInspectFunc(ctx, networkID, options) } return types.NetworkResource{}, nil } // NetworkInspectWithRaw fakes inspecting a network with a raw response -func (c *FakeClient) NetworkInspectWithRaw(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, []byte, error) { +func (c *FakeClient) NetworkInspectWithRaw(_ context.Context, _ string, _ types.NetworkInspectOptions) (types.NetworkResource, []byte, error) { return types.NetworkResource{}, nil, nil } // NetworkList fakes listing networks -func (c *FakeClient) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) { +func (c *FakeClient) NetworkList(_ context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) { return nil, nil } @@ -51,6 +51,6 @@ func (c *FakeClient) NetworkRemove(ctx context.Context, networkID string) error } // NetworksPrune fakes pruning networks -func (c *FakeClient) NetworksPrune(ctx context.Context, pruneFilter filters.Args) (types.NetworksPruneReport, error) { +func (c *FakeClient) NetworksPrune(_ context.Context, pruneFilter filters.Args) (types.NetworksPruneReport, error) { return types.NetworksPruneReport{}, nil } diff --git a/components/cli/cli/internal/test/store.go b/components/cli/cli/internal/test/store.go index 28e52bab05..e5b3de7abb 100644 --- a/components/cli/cli/internal/test/store.go +++ b/components/cli/cli/internal/test/store.go @@ -53,8 +53,7 @@ func (c *fakeStore) Get(serverAddress string) (types.AuthConfig, error) { if c.getFunc != nil { return c.getFunc(serverAddress) } - authConfig, _ := c.store[serverAddress] - return authConfig, nil + return c.store[serverAddress], nil } func (c *fakeStore) GetAll() (map[string]types.AuthConfig, error) { diff --git a/components/cli/cli/trust/trust.go b/components/cli/cli/trust/trust.go index c325b966a8..600a2acc41 100644 --- a/components/cli/cli/trust/trust.go +++ b/components/cli/cli/trust/trust.go @@ -161,7 +161,7 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI } tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions) basicHandler := auth.NewBasicHandler(creds) - modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))) + modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) tr := transport.NewTransport(base, modifiers...) return client.NewNotaryRepository( diff --git a/components/cli/codecov.yml b/components/cli/codecov.yml index 82e1ed8018..ec7d4e2127 100644 --- a/components/cli/codecov.yml +++ b/components/cli/codecov.yml @@ -11,7 +11,7 @@ coverage: project: default: target: auto - threshold: "0.05%" + threshold: "15%" changes: false ignore: - "**/internal/test" diff --git a/components/cli/contrib/completion/zsh/_docker b/components/cli/contrib/completion/zsh/_docker index 0860907839..4b0c1e25c7 100644 --- a/components/cli/contrib/completion/zsh/_docker +++ b/components/cli/contrib/completion/zsh/_docker @@ -2620,7 +2620,7 @@ __docker_subcommand() { "($help)--default-gateway-v6[Container default gateway IPv6 address]:IPv6 address: " \ "($help)--default-shm-size=[Default shm size for containers]:size:" \ "($help)*--default-ulimit=[Default ulimits for containers]:ulimit: " \ - "($help)--disable-legacy-registry[Disable contacting legacy registries]" \ + "($help)--disable-legacy-registry[Disable contacting legacy registries (default true)]" \ "($help)*--dns=[DNS server to use]:DNS: " \ "($help)*--dns-opt=[DNS options to use]:DNS option: " \ "($help)*--dns-search=[DNS search domains to use]:DNS search: " \ diff --git a/components/cli/docker.Makefile b/components/cli/docker.Makefile index 765b3a6f96..ef8dbfb926 100644 --- a/components/cli/docker.Makefile +++ b/components/cli/docker.Makefile @@ -7,7 +7,7 @@ DEV_DOCKER_IMAGE_NAME = docker-cli-dev LINTER_IMAGE_NAME = docker-cli-lint CROSS_IMAGE_NAME = docker-cli-cross -MOUNTS = -v `pwd`:/go/src/github.com/docker/cli +MOUNTS = -v "$(CURDIR)":/go/src/github.com/docker/cli VERSION = $(shell cat VERSION) ENVVARS = -e VERSION=$(VERSION) -e GITCOMMIT @@ -69,4 +69,14 @@ vendor: build_docker_image vendor.conf docker run -ti --rm $(MOUNTS) $(DEV_DOCKER_IMAGE_NAME) make vendor dynbinary: build_cross_image - docker run --rm $(ENVVARS) $(MOUNTS) $(CROSS_IMAGE_NAME) make dynbinary + docker run -ti --rm $(ENVVARS) $(MOUNTS) $(CROSS_IMAGE_NAME) make dynbinary + +## generate man pages from go source and markdown +.PHONY: manpages +manpages: build_docker_image + docker run -ti --rm $(MOUNTS) $(DEV_DOCKER_IMAGE_NAME) make manpages + +## Generate documentation YAML files consumed by docs repo +.PHONY: yamldocs +yamldocs: build_docker_image + docker run -ti --rm $(MOUNTS) $(DEV_DOCKER_IMAGE_NAME) make yamldocs diff --git a/components/cli/dockerfiles/Dockerfile.cross b/components/cli/dockerfiles/Dockerfile.cross index f52c3e10dd..19907ced49 100644 --- a/components/cli/dockerfiles/Dockerfile.cross +++ b/components/cli/dockerfiles/Dockerfile.cross @@ -1,5 +1,5 @@ -FROM golang:1.8.1 +FROM golang:1.8.3 # allow replacing httpredir or deb mirror ARG APT_MIRROR=deb.debian.org diff --git a/components/cli/dockerfiles/Dockerfile.dev b/components/cli/dockerfiles/Dockerfile.dev index f3e058e285..aef37382c8 100644 --- a/components/cli/dockerfiles/Dockerfile.dev +++ b/components/cli/dockerfiles/Dockerfile.dev @@ -1,5 +1,5 @@ -FROM golang:1.8-alpine +FROM golang:1.8.3-alpine RUN apk add -U git make bash coreutils diff --git a/components/cli/dockerfiles/Dockerfile.lint b/components/cli/dockerfiles/Dockerfile.lint index 2bba9595e5..240af474c5 100644 --- a/components/cli/dockerfiles/Dockerfile.lint +++ b/components/cli/dockerfiles/Dockerfile.lint @@ -1,11 +1,12 @@ -FROM golang:1.8-alpine +FROM golang:1.8.3-alpine RUN apk add -U git -RUN go get -u gopkg.in/alecthomas/gometalinter.v1 && \ +RUN go get -u gopkg.in/dnephin/gometalinter.v1 && \ mv /go/bin/gometalinter.v1 /usr/local/bin/gometalinter && \ gometalinter --install WORKDIR /go/src/github.com/docker/cli +ENV CGO_ENABLED=0 ENTRYPOINT ["/usr/local/bin/gometalinter"] -CMD ["--config=gometalinter.json", "./..."] +CMD ["--config=gometalinter.json", "./..."] diff --git a/components/cli/dockerfiles/osx-cross.sh b/components/cli/dockerfiles/osx-cross.sh index 07f6542fcc..840334a135 100755 --- a/components/cli/dockerfiles/osx-cross.sh +++ b/components/cli/dockerfiles/osx-cross.sh @@ -25,5 +25,5 @@ echo "Downloading OSX SDK" time curl -sSL https://s3.dockerproject.org/darwin/v2/${OSX_SDK}.tar.xz \ -o "${OSXCROSS_PATH}/tarballs/${OSX_SDK}.tar.xz" -echo "Buidling osxcross" +echo "Building osxcross" UNATTENDED=yes OSX_VERSION_MIN=10.6 ${OSXCROSS_PATH}/build.sh > /dev/null diff --git a/components/cli/docs/deprecated.md b/components/cli/docs/deprecated.md index 7e0bfc0a60..9905994b73 100644 --- a/components/cli/docs/deprecated.md +++ b/components/cli/docs/deprecated.md @@ -138,7 +138,7 @@ on all subcommands (due to it conflicting with, e.g. `-h` / `--hostname` on ### `-e` and `--email` flags on `docker login` **Deprecated In Release: [v1.11.0](https://github.com/docker/docker/releases/tag/v1.11.0)** -**Target For Removal In Release: v17.06** +**Removed In Release: [v17.06](https://github.com/docker/docker-ce/releases/tag/v17.06.0-ce)** The docker login command is removing the ability to automatically register for an account with the target registry if the given username doesn't exist. Due to this change, the email flag is no longer required, and will be deprecated. @@ -292,7 +292,7 @@ of the `--changes` flag that allows to pass `Dockerfile` commands. **Target For Removal In Release: v17.12** -Version 1.9 adds a flag (`--disable-legacy-registry=false`) which prevents the +Version 1.8.3 added a flag (`--disable-legacy-registry=false`) which prevents the docker daemon from `pull`, `push`, and `login` operations against v1 registries. Though enabled by default, this signals the intent to deprecate the v1 protocol. diff --git a/components/cli/docs/extend/legacy_plugins.md b/components/cli/docs/extend/legacy_plugins.md index 68bba59f46..dc77743b86 100644 --- a/components/cli/docs/extend/legacy_plugins.md +++ b/components/cli/docs/extend/legacy_plugins.md @@ -87,8 +87,9 @@ Plugin Plugin | Description ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - [Twistlock AuthZ Broker](https://github.com/twistlock/authz) | A basic extendable authorization plugin that runs directly on the host or inside a container. This plugin allows you to define user policies that it evaluates during authorization. Basic authorization is provided if Docker daemon is started with the --tlsverify flag (username is extracted from the certificate common name). - [HBM plugin](https://github.com/kassisol/hbm) | An authorization plugin that prevents from executing commands with certains parameters. +[Casbin AuthZ Plugin](https://github.com/casbin/casbin-authz-plugin) | An authorization plugin based on [Casbin](https://github.com/casbin/casbin), which supports access control models like ACL, RBAC, ABAC. The access control model can be customized. The policy can be persisted into file or DB. +[HBM plugin](https://github.com/kassisol/hbm) | An authorization plugin that prevents from executing commands with certains parameters. +[Twistlock AuthZ Broker](https://github.com/twistlock/authz) | A basic extendable authorization plugin that runs directly on the host or inside a container. This plugin allows you to define user policies that it evaluates during authorization. Basic authorization is provided if Docker daemon is started with the --tlsverify flag (username is extracted from the certificate common name). ## Troubleshooting a plugin diff --git a/components/cli/docs/extend/plugins_authorization.md b/components/cli/docs/extend/plugins_authorization.md index ac1837f754..5b49d657ef 100644 --- a/components/cli/docs/extend/plugins_authorization.md +++ b/components/cli/docs/extend/plugins_authorization.md @@ -27,7 +27,7 @@ same is true for callers using Docker's Engine API to contact the daemon. If you require greater access control, you can create authorization plugins and add them to your Docker daemon configuration. Using an authorization plugin, a Docker administrator can configure granular access policies for managing access -to Docker daemon. +to the Docker daemon. Anyone with the appropriate skills can develop an authorization plugin. These skills, at their most basic, are knowledge of Docker, understanding of REST, and diff --git a/components/cli/docs/reference/builder.md b/components/cli/docs/reference/builder.md index 2571511b64..9ab4ba4df9 100644 --- a/components/cli/docs/reference/builder.md +++ b/components/cli/docs/reference/builder.md @@ -94,8 +94,8 @@ instructions. Whenever possible, Docker will re-use the intermediate images (cache), to accelerate the `docker build` process significantly. This is indicated by the `Using cache` message in the console output. -(For more information, see the [Build cache section](https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#/build-cache)) in the -`Dockerfile` best practices guide: +(For more information, see the [Build cache section](https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#build-cache) in the +`Dockerfile` best practices guide): $ docker build -t svendowideit/ambassador . Sending build context to Docker daemon 15.36 kB @@ -1281,26 +1281,43 @@ This Dockerfile results in an image that causes `docker run`, to create a new mount point at `/myvol` and copy the `greeting` file into the newly created volume. -> **Note**: -> When using Windows-based containers, the destination of a volume inside the -> container must be one of: a non-existing or empty directory; or a drive other -> than C:. +### Notes about specifying volumes -> **Note**: -> If any build steps change the data within the volume after it has been -> declared, those changes will be discarded. +Keep the following things in mind about volumes in the `Dockerfile`. -> **Note**: -> The list is parsed as a JSON array, which means that -> you must use double-quotes (") around words not single-quotes ('). +- **Volumes on Windows-based containers**: When using Windows-based containers, + the destination of a volume inside the container must be one of: + + - a non-existing or empty directory + - a drive other than `C:` + +- **Changing the volume from within the Dockerfile**: If any build steps change the + data within the volume after it has been declared, those changes will be discarded. + +- **JSON formatting**: The list is parsed as a JSON array. + You must enclose words with double quotes (`"`)rather than single quotes (`'`). + +- **The host directory is declared at container run-time**: The host directory + (the mountpoint) is, by its nature, host-dependent. This is to preserve image + portability. since a given host directory can't be guaranteed to be available + on all hosts.For this reason, you can't mount a host directory from + within the Dockerfile. The `VOLUME` instruction does not support specifying a `host-dir` + parameter. You must specify the mountpoint when you create or run the container. ## USER - USER daemon + USER [:] +or + USER [:] + +The `USER` instruction sets the user name (or UID) and optionally the user +group (or GID) to use when running the image and for any `RUN`, `CMD` and +`ENTRYPOINT` instructions that follow it in the `Dockerfile`. + +> **Warning**: +> When the user does doesn't have a primary group then the image (or the next +> instructions) will be run with the `root` group. -The `USER` instruction sets the user name or UID to use when running the image -and for any `RUN`, `CMD` and `ENTRYPOINT` instructions that follow it in the -`Dockerfile`. ## WORKDIR diff --git a/components/cli/docs/reference/commandline/attach.md b/components/cli/docs/reference/commandline/attach.md index 4153331bad..7e654d7350 100644 --- a/components/cli/docs/reference/commandline/attach.md +++ b/components/cli/docs/reference/commandline/attach.md @@ -40,7 +40,7 @@ interactively, as though the commands were running directly in your terminal. > not be interacting with the terminal at that time. You can attach to the same contained process multiple times simultaneously, -even as a different user with the appropriate permissions. +from different sessions on the Docker host. To stop a container, use `CTRL-c`. This key sequence sends `SIGKILL` to the container. If `--sig-proxy` is true (the default),`CTRL-c` sends a `SIGINT` to diff --git a/components/cli/docs/reference/commandline/dockerd.md b/components/cli/docs/reference/commandline/dockerd.md index 93774c841b..ef6bcd3831 100644 --- a/components/cli/docs/reference/commandline/dockerd.md +++ b/components/cli/docs/reference/commandline/dockerd.md @@ -42,7 +42,7 @@ Options: --default-gateway-v6 ip Container default gateway IPv6 address --default-runtime string Default OCI runtime for containers (default "runc") --default-ulimit ulimit Default ulimits for containers (default []) - --disable-legacy-registry Disable contacting legacy registries + --disable-legacy-registry Disable contacting legacy registries (default true) --dns list DNS server to use (default []) --dns-opt list DNS options to use (default []) --dns-search list DNS search domains to use (default []) @@ -901,7 +901,18 @@ system's list of trusted CAs instead of enabling `--insecure-registry`. ##### Legacy Registries -Enabling `--disable-legacy-registry` forces a docker daemon to only interact with registries which support the V2 protocol. Specifically, the daemon will not attempt `push`, `pull` and `login` to v1 registries. The exception to this is `search` which can still be performed on v1 registries. +Operations against registries supporting only the legacy v1 protocol are +disabled by default. Specifically, the daemon will not attempt `push`, +`pull` and `login` to v1 registries. The exception to this is `search` +which can still be performed on v1 registries. + +Add `"disable-legacy-registry":false` to the [daemon configuration +file](#daemon-configuration-file), or set the +`--disable-legacy-registry=false` flag, if you need to interact with +registries that have not yet migrated to the v2 protocol. + +Interaction v1 registries will no longer be supported in Docker v17.12, +and the `disable-legacy-registry` configuration option will be removed. #### Running a Docker daemon behind an HTTPS_PROXY diff --git a/components/cli/docs/reference/commandline/exec.md b/components/cli/docs/reference/commandline/exec.md index e2e5d607b9..2bfa74483c 100644 --- a/components/cli/docs/reference/commandline/exec.md +++ b/components/cli/docs/reference/commandline/exec.md @@ -76,6 +76,17 @@ $ docker exec -it ubuntu_bash bash This will create a new Bash session in the container `ubuntu_bash`. +Next, set an environment variable in the current bash session. + +```bash +$ docker exec -it -e VAR=1 ubuntu_bash bash +``` + +This will create a new Bash session in the container `ubuntu_bash` with environment +variable `$VAR` set to "1". Note that this environment variable will only be valid +on the current Bash session. + + ### Try to run `docker exec` on a paused container If the container is paused, then the `docker exec` command will fail with an error: diff --git a/components/cli/docs/reference/commandline/node_ls.md b/components/cli/docs/reference/commandline/node_ls.md index 224642c2e5..2f9c2becdf 100644 --- a/components/cli/docs/reference/commandline/node_ls.md +++ b/components/cli/docs/reference/commandline/node_ls.md @@ -89,7 +89,7 @@ ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS 1bcef6utixb0l0ca7gxuivsj0 swarm-worker2 Ready Active ``` -#### membersip +#### membership The `membership` filter matches nodes based on the presence of a `membership` and a value `accepted` or `pending`. diff --git a/components/cli/docs/reference/commandline/run.md b/components/cli/docs/reference/commandline/run.md index ecb676dc20..2216319415 100644 --- a/components/cli/docs/reference/commandline/run.md +++ b/components/cli/docs/reference/commandline/run.md @@ -745,6 +745,41 @@ PS C:\> docker run -d --isolation default microsoft/nanoserver powershell echo h PS C:\> docker run -d --isolation hyperv microsoft/nanoserver powershell echo hyperv ``` +### Specify hard limits on memory available to containers (-m, --memory) + +These parameters always set an upper limit on the memory available to the container. On Linux, this +is set on the cgroup and applications in a container can query it at `/sys/fs/cgroup/memory/memory.limit_in_bytes`. + +On Windows, this will affect containers differently depending on what type of isolation is used. + +- With `process` isolation, Windows will report the full memory of the host system, not the limit to applications running inside the container + ```powershell + docker run -it -m 2GB --isolation=process microsoft/nanoserver powershell Get-ComputerInfo *memory* + + CsTotalPhysicalMemory : 17064509440 + CsPhyicallyInstalledMemory : 16777216 + OsTotalVisibleMemorySize : 16664560 + OsFreePhysicalMemory : 14646720 + OsTotalVirtualMemorySize : 19154928 + OsFreeVirtualMemory : 17197440 + OsInUseVirtualMemory : 1957488 + OsMaxProcessMemorySize : 137438953344 + ``` +- With `hyperv` isolation, Windows will create a utility VM that is big enough to hold the memory limit, plus the minimal OS needed to host the container. That size is reported as "Total Physical Memory." + ```powershell + docker run -it -m 2GB --isolation=hyperv microsoft/nanoserver powershell Get-ComputerInfo *memory* + + CsTotalPhysicalMemory : 2683355136 + CsPhyicallyInstalledMemory : + OsTotalVisibleMemorySize : 2620464 + OsFreePhysicalMemory : 2306552 + OsTotalVirtualMemorySize : 2620464 + OsFreeVirtualMemory : 2356692 + OsInUseVirtualMemory : 263772 + OsMaxProcessMemorySize : 137438953344 + ``` + + ### Configure namespaced kernel parameters (sysctls) at runtime The `--sysctl` sets namespaced kernel parameters (sysctls) in the diff --git a/components/cli/docs/reference/commandline/stack_deploy.md b/components/cli/docs/reference/commandline/stack_deploy.md index d57ef0f76c..7f3f7f4c5b 100644 --- a/components/cli/docs/reference/commandline/stack_deploy.md +++ b/components/cli/docs/reference/commandline/stack_deploy.md @@ -40,7 +40,7 @@ has to be run targeting a manager node. ### Compose file -The `deploy` command supports compose file version `3.0` and above." +The `deploy` command supports compose file version `3.0` and above. ```bash $ docker stack deploy --compose-file docker-compose.yml vossibility @@ -57,7 +57,28 @@ Creating service vossibility_ghollector Creating service vossibility_lookupd ``` -You can verify that the services were correctly created +Only a single Compose file is accepted. If your configuration is split between +multiple Compose files, e.g. a base configuration and environment-specific overrides, +you can combine these by passing them to `docker-compose config` with the `-f` option +and redirecting the merged output into a new file. + +```bash +$ docker-compose -f docker-compose.yml -f docker-compose.prod.yml config > docker-stack.yml +$ docker stack deploy --compose-file docker-stack.yml vossibility + +Ignoring unsupported options: links + +Creating network vossibility_vossibility +Creating network vossibility_default +Creating service vossibility_nsqd +Creating service vossibility_logstash +Creating service vossibility_elasticsearch +Creating service vossibility_kibana +Creating service vossibility_ghollector +Creating service vossibility_lookupd +``` + +You can verify that the services were correctly created: ```bash $ docker service ls diff --git a/components/cli/docs/reference/run.md b/components/cli/docs/reference/run.md index b209343b58..817c20c969 100644 --- a/components/cli/docs/reference/run.md +++ b/components/cli/docs/reference/run.md @@ -1123,7 +1123,7 @@ by default a container is not allowed to access any devices, but a the documentation on [cgroups devices](https://www.kernel.org/doc/Documentation/cgroup-v1/devices.txt)). When the operator executes `docker run --privileged`, Docker will enable -to access to all devices on the host as well as set some configuration +access to all devices on the host as well as set some configuration in AppArmor or SELinux to allow the container nearly all the same access to the host as processes running outside containers on the host. Additional information about running with `--privileged` is available on the diff --git a/components/cli/docs/yaml/yaml.go b/components/cli/docs/yaml/yaml.go index 575f9bec5c..edf5e9dd7f 100644 --- a/components/cli/docs/yaml/yaml.go +++ b/components/cli/docs/yaml/yaml.go @@ -66,14 +66,14 @@ func GenYamlTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandle if _, err := io.WriteString(f, filePrepender(filename)); err != nil { return err } - if err := GenYamlCustom(cmd, f, linkHandler); err != nil { + if err := GenYamlCustom(cmd, f); err != nil { return err } return nil } // GenYamlCustom creates custom yaml output -func GenYamlCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error { +func GenYamlCustom(cmd *cobra.Command, w io.Writer) error { cliDoc := cmdDoc{} cliDoc.Name = cmd.CommandPath() diff --git a/components/cli/experimental/docker-stacks-and-bundles.md b/components/cli/experimental/docker-stacks-and-bundles.md index 8abfd76359..1271af67e0 100644 --- a/components/cli/experimental/docker-stacks-and-bundles.md +++ b/components/cli/experimental/docker-stacks-and-bundles.md @@ -161,21 +161,18 @@ A service has the following fields: only specify the container port to be exposed. These ports can be mapped on runtime hosts at the operator's discretion. -
WorkingDir string
Working directory inside the service containers.
-
User string
Username or UID (format: <name|uid>[:<group|gid>]).
-
Networks []string
diff --git a/components/cli/gometalinter.json b/components/cli/gometalinter.json index f7c6a88a5d..b5956dca60 100644 --- a/components/cli/gometalinter.json +++ b/components/cli/gometalinter.json @@ -11,9 +11,14 @@ "gofmt", "goimports", "golint", + "gosimple", "ineffassign", "interfacer", "lll", + "misspell", + "unconvert", + "unparam", + "unused", "vet" ], diff --git a/components/cli/man/dockerd.8.md b/components/cli/man/dockerd.8.md index a4e079074f..e9d7e68739 100644 --- a/components/cli/man/dockerd.8.md +++ b/components/cli/man/dockerd.8.md @@ -192,7 +192,7 @@ $ sudo dockerd --add-runtime runc=runc --add-runtime custom=/usr/local/bin/my-ru Default ulimits for containers. **--disable-legacy-registry**=*true*|*false* - Disable contacting legacy registries + Disable contacting legacy registries. Default is `true`. **--dns**="" Force Docker to use specific DNS servers diff --git a/components/cli/man/generate.sh b/components/cli/man/generate.sh deleted file mode 100755 index 905b2d7baf..0000000000 --- a/components/cli/man/generate.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -# -# Generate man pages for docker/docker -# - -set -eu - -mkdir -p ./man/man1 - -# Generate man pages from cobra commands -go build -o /tmp/gen-manpages ./man -/tmp/gen-manpages --root . --target ./man/man1 - -# Generate legacy pages from markdown -./man/md2man-all.sh -q diff --git a/components/cli/man/import.go b/components/cli/man/import.go new file mode 100644 index 0000000000..bc117bdcfb --- /dev/null +++ b/components/cli/man/import.go @@ -0,0 +1,7 @@ +// +build never + +package main + +// Not used, but required for generating other man pages. +// Import it here so that the package is included by vndr. +import _ "github.com/cpuguy83/go-md2man" diff --git a/components/cli/vendor/github.com/docker/docker/runconfig/opts/envfile.go b/components/cli/opts/envfile.go similarity index 100% rename from components/cli/vendor/github.com/docker/docker/runconfig/opts/envfile.go rename to components/cli/opts/envfile.go diff --git a/components/cli/opts/envfile_test.go b/components/cli/opts/envfile_test.go new file mode 100644 index 0000000000..f3faabe3c0 --- /dev/null +++ b/components/cli/opts/envfile_test.go @@ -0,0 +1,141 @@ +package opts + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "reflect" + "strings" + "testing" +) + +func tmpFileWithContent(content string, t *testing.T) string { + tmpFile, err := ioutil.TempFile("", "envfile-test") + if err != nil { + t.Fatal(err) + } + defer tmpFile.Close() + + tmpFile.WriteString(content) + return tmpFile.Name() +} + +// Test ParseEnvFile for a file with a few well formatted lines +func TestParseEnvFileGoodFile(t *testing.T) { + content := `foo=bar + baz=quux +# comment + +_foobar=foobaz +with.dots=working +and_underscore=working too +` + // Adding a newline + a line with pure whitespace. + // This is being done like this instead of the block above + // because it's common for editors to trim trailing whitespace + // from lines, which becomes annoying since that's the + // exact thing we need to test. + content += "\n \t " + tmpFile := tmpFileWithContent(content, t) + defer os.Remove(tmpFile) + + lines, err := ParseEnvFile(tmpFile) + if err != nil { + t.Fatal(err) + } + + expectedLines := []string{ + "foo=bar", + "baz=quux", + "_foobar=foobaz", + "with.dots=working", + "and_underscore=working too", + } + + if !reflect.DeepEqual(lines, expectedLines) { + t.Fatal("lines not equal to expectedLines") + } +} + +// Test ParseEnvFile for an empty file +func TestParseEnvFileEmptyFile(t *testing.T) { + tmpFile := tmpFileWithContent("", t) + defer os.Remove(tmpFile) + + lines, err := ParseEnvFile(tmpFile) + if err != nil { + t.Fatal(err) + } + + if len(lines) != 0 { + t.Fatal("lines not empty; expected empty") + } +} + +// Test ParseEnvFile for a non existent file +func TestParseEnvFileNonExistentFile(t *testing.T) { + _, err := ParseEnvFile("foo_bar_baz") + if err == nil { + t.Fatal("ParseEnvFile succeeded; expected failure") + } + if _, ok := err.(*os.PathError); !ok { + t.Fatalf("Expected a PathError, got [%v]", err) + } +} + +// Test ParseEnvFile for a badly formatted file +func TestParseEnvFileBadlyFormattedFile(t *testing.T) { + content := `foo=bar + f =quux +` + + tmpFile := tmpFileWithContent(content, t) + defer os.Remove(tmpFile) + + _, err := ParseEnvFile(tmpFile) + if err == nil { + t.Fatalf("Expected an ErrBadEnvVariable, got nothing") + } + if _, ok := err.(ErrBadEnvVariable); !ok { + t.Fatalf("Expected an ErrBadEnvVariable, got [%v]", err) + } + expectedMessage := "poorly formatted environment: variable 'f ' has white spaces" + if err.Error() != expectedMessage { + t.Fatalf("Expected [%v], got [%v]", expectedMessage, err.Error()) + } +} + +// Test ParseEnvFile for a file with a line exceeding bufio.MaxScanTokenSize +func TestParseEnvFileLineTooLongFile(t *testing.T) { + content := strings.Repeat("a", bufio.MaxScanTokenSize+42) + content = fmt.Sprint("foo=", content) + + tmpFile := tmpFileWithContent(content, t) + defer os.Remove(tmpFile) + + _, err := ParseEnvFile(tmpFile) + if err == nil { + t.Fatal("ParseEnvFile succeeded; expected failure") + } +} + +// ParseEnvFile with a random file, pass through +func TestParseEnvFileRandomFile(t *testing.T) { + content := `first line +another invalid line` + tmpFile := tmpFileWithContent(content, t) + defer os.Remove(tmpFile) + + _, err := ParseEnvFile(tmpFile) + if err == nil { + t.Fatalf("Expected an ErrBadEnvVariable, got nothing") + } + if _, ok := err.(ErrBadEnvVariable); !ok { + t.Fatalf("Expected an ErrBadEnvVariable, got [%v]", err) + } + expectedMessage := "poorly formatted environment: variable 'first line' has white spaces" + if err.Error() != expectedMessage { + t.Fatalf("Expected [%v], got [%v]", expectedMessage, err.Error()) + } +} diff --git a/components/cli/opts/opts.go b/components/cli/opts/opts.go index f76f308051..d915ec2f76 100644 --- a/components/cli/opts/opts.go +++ b/components/cli/opts/opts.go @@ -179,7 +179,7 @@ func (opts *MapOpts) GetAll() map[string]string { } func (opts *MapOpts) String() string { - return fmt.Sprintf("%v", map[string]string((opts.values))) + return fmt.Sprintf("%v", opts.values) } // Type returns a string name for this Option type diff --git a/components/cli/opts/opts_test.go b/components/cli/opts/opts_test.go index f7b91db928..723a212ea7 100644 --- a/components/cli/opts/opts_test.go +++ b/components/cli/opts/opts_test.go @@ -158,7 +158,7 @@ func TestValidateDNSSearch(t *testing.T) { `foo.bar-.baz`, `foo.-bar`, `foo.-bar.baz`, - `foo.bar.baz.this.should.fail.on.long.name.beause.it.is.longer.thanisshouldbethis.should.fail.on.long.name.beause.it.is.longer.thanisshouldbethis.should.fail.on.long.name.beause.it.is.longer.thanisshouldbethis.should.fail.on.long.name.beause.it.is.longer.thanisshouldbe`, + `foo.bar.baz.this.should.fail.on.long.name.because.it.is.longer.thanisshouldbethis.should.fail.on.long.name.because.it.is.longer.thanisshouldbethis.should.fail.on.long.name.because.it.is.longer.thanisshouldbethis.should.fail.on.long.name.because.it.is.longer.thanisshouldbe`, } for _, domain := range valid { diff --git a/components/cli/vendor/github.com/docker/docker/runconfig/opts/parse.go b/components/cli/opts/parse.go similarity index 100% rename from components/cli/vendor/github.com/docker/docker/runconfig/opts/parse.go rename to components/cli/opts/parse.go diff --git a/components/cli/opts/quotedstring.go b/components/cli/opts/quotedstring.go index fb1e5374bc..09c68a5261 100644 --- a/components/cli/opts/quotedstring.go +++ b/components/cli/opts/quotedstring.go @@ -18,7 +18,7 @@ func (s *QuotedString) Type() string { } func (s *QuotedString) String() string { - return string(*s.value) + return *s.value } func trimQuotes(value string) string { diff --git a/components/cli/opts/throttledevice.go b/components/cli/opts/throttledevice.go index 65dd3ebf68..0959efae35 100644 --- a/components/cli/opts/throttledevice.go +++ b/components/cli/opts/throttledevice.go @@ -52,10 +52,7 @@ func ValidateThrottleIOpsDevice(val string) (*blkiodev.ThrottleDevice, error) { return nil, fmt.Errorf("invalid rate for device: %s. The correct format is :. Number must be a positive integer", val) } - return &blkiodev.ThrottleDevice{ - Path: split[0], - Rate: uint64(rate), - }, nil + return &blkiodev.ThrottleDevice{Path: split[0], Rate: rate}, nil } // ThrottledeviceOpt defines a map of ThrottleDevices diff --git a/components/cli/opts/weightdevice.go b/components/cli/opts/weightdevice.go index 7e3d064f27..46ce9b6567 100644 --- a/components/cli/opts/weightdevice.go +++ b/components/cli/opts/weightdevice.go @@ -75,12 +75,7 @@ func (opt *WeightdeviceOpt) String() string { // GetList returns a slice of pointers to WeightDevices. func (opt *WeightdeviceOpt) GetList() []*blkiodev.WeightDevice { - var weightdevice []*blkiodev.WeightDevice - for _, v := range opt.values { - weightdevice = append(weightdevice, v) - } - - return weightdevice + return opt.values } // Type returns the option type diff --git a/components/cli/scripts/docs/generate-man.sh b/components/cli/scripts/docs/generate-man.sh new file mode 100755 index 0000000000..eb0a68da51 --- /dev/null +++ b/components/cli/scripts/docs/generate-man.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Generate man pages for docker/cli +set -eu -o pipefail + +mkdir -p ./man/man1 + +go install ./vendor/github.com/cpuguy83/go-md2man + +# Generate man pages from cobra commands +go build -o /tmp/gen-manpages github.com/docker/cli/man +/tmp/gen-manpages --root $(pwd) --target $(pwd)/man/man1 + +# Generate legacy pages from markdown +./man/md2man-all.sh -q diff --git a/components/cli/scripts/docs/generate-yaml.sh b/components/cli/scripts/docs/generate-yaml.sh new file mode 100755 index 0000000000..a8afed1796 --- /dev/null +++ b/components/cli/scripts/docs/generate-yaml.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Generate yaml for docker/cli reference docs +set -eu -o pipefail + +mkdir -p docs/yaml/gen + +go build -o build/yaml-docs-generator github.com/docker/cli/docs/yaml +build/yaml-docs-generator --root $(pwd) --target $(pwd)/docs/yaml/gen diff --git a/components/cli/scripts/test/unit-with-coverage b/components/cli/scripts/test/unit-with-coverage index 9073e2daf6..cd1ead8477 100755 --- a/components/cli/scripts/test/unit-with-coverage +++ b/components/cli/scripts/test/unit-with-coverage @@ -1,7 +1,11 @@ #!/usr/bin/env bash set -eu -o pipefail -for pkg in $(go list ./... | grep -v /vendor/); do +# install test dependencies once before running tests for each package. This +# reduces the runtime from 200s down to 23s +go test -i $@ + +for pkg in $@; do ./scripts/test/unit \ -cover \ -coverprofile=profile.out \ diff --git a/components/cli/scripts/validate/check-git-diff b/components/cli/scripts/validate/check-git-diff index e41970a35a..4cdc0d2db6 100755 --- a/components/cli/scripts/validate/check-git-diff +++ b/components/cli/scripts/validate/check-git-diff @@ -3,12 +3,13 @@ set -eu DIFF_PATH=$1 +DIFF=$(git status --porcelain -- $DIFF_PATH) -if [ "`git status --porcelain -- $DIFF_PATH 2>/dev/null`" ]; then +if [ "$DIFF" ]; then echo echo "These files were changed:" echo - git status --porcelain -- $DIFF_PATH 2>/dev/null + echo $DIFF echo exit 1 else diff --git a/components/cli/vendor.conf b/components/cli/vendor.conf old mode 100644 new mode 100755 index 83249b101e..5a2c93c3f5 --- a/components/cli/vendor.conf +++ b/components/cli/vendor.conf @@ -1,5 +1,5 @@ github.com/Azure/go-ansiterm 388960b655244e76e24c75f48631564eaefade62 -github.com/Microsoft/go-winio v0.4.0 +github.com/Microsoft/go-winio v0.4.1 github.com/Nvveen/Gotty a8b993ba6abdb0e0c12b0125c603323a71c7790c https://github.com/ijc25/Gotty github.com/Sirupsen/logrus v0.11.0 github.com/agl/ed25519 d2b94fd789ea21d12fac1a4443dd3a3f79cda72c @@ -7,13 +7,12 @@ github.com/coreos/etcd 824277cb3a577a0e8c829ca9ec557b973fe06d20 github.com/cpuguy83/go-md2man a65d4d2de4d5f7c74868dfa9b202a3c8be315aaa github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76 github.com/docker/distribution b38e5838b7b2f2ad48e06ec4b500011976080621 -github.com/docker/docker 45c6f4262a865a03adaac291a9ce33c0f2190d77 -github.com/docker/docker-credential-helpers v0.5.0 -github.com/docker/go d30aec9fd63c35133f8f79c3412ad91a3b08be06 +github.com/docker/docker 4310f7da7e6bcd8185bf05e032f9b7321cfa6ea2 +github.com/docker/docker-credential-helpers v0.5.1 +github.com/docker/go d30aec9fd63c35133f8f79c3412ad91a3b08be06 #? github.com/docker/go-connections e15c02316c12de00874640cd76311849de2aeed5 github.com/docker/go-events 18b43f1bc85d9cdd42c05a6cd2d444c7a200a894 github.com/docker/go-units 9e638d38cf6977a37a8ea0078f3ee75a7cdb2dd1 -github.com/docker/libnetwork b13e0604016a4944025aaff521d9c125850b0d04 github.com/docker/libtrust 9cbd2a1374f46905c68a4eb3694a130610adc62a github.com/docker/notary v0.4.2 github.com/docker/swarmkit 1a3e510517be82d18ac04380b5f71eddf06c2fc0 @@ -29,7 +28,7 @@ github.com/mitchellh/mapstructure f3009df150dadf309fdee4a54ed65c124afad715 github.com/opencontainers/go-digest a6d0ee40d4207ea02364bd3b9e8e77b9159ba1eb github.com/opencontainers/image-spec f03dbe35d449c54915d235f1a3cf8f585a24babe github.com/opencontainers/runc 9c2d8d184e5da67c95d601382adf14862e4f2228 https://github.com/docker/runc.git -github.com/opencontainers/selinux ba1aefe8057f1d0cfb8e88d0ec1dc85925ef987d +github.com/opencontainers/selinux v1.0.0-rc1 github.com/pkg/errors 839d9e913e063e28dfd0e6c7b7512793e0a48be9 github.com/pmezard/go-difflib 792786c7400a136282c1664665ae0a8db921c6c2 github.com/russross/blackfriday 1d6b8e9301e720b08a8938b8c25c018285885438 diff --git a/components/cli/vendor/github.com/Microsoft/go-winio/file.go b/components/cli/vendor/github.com/Microsoft/go-winio/file.go index 078a5687b6..613f31b520 100644 --- a/components/cli/vendor/github.com/Microsoft/go-winio/file.go +++ b/components/cli/vendor/github.com/Microsoft/go-winio/file.go @@ -124,7 +124,8 @@ func (f *win32File) Close() error { return nil } -// prepareIo prepares for a new IO operation +// prepareIo prepares for a new IO operation. +// The caller must call f.wg.Done() when the IO is finished, prior to Close() returning. func (f *win32File) prepareIo() (*ioOperation, error) { f.wg.Add(1) if f.closing { @@ -155,7 +156,6 @@ func ioCompletionProcessor(h syscall.Handle) { // the operation has actually completed. func (f *win32File) asyncIo(c *ioOperation, d *deadlineHandler, bytes uint32, err error) (int, error) { if err != syscall.ERROR_IO_PENDING { - f.wg.Done() return int(bytes), err } @@ -192,7 +192,6 @@ func (f *win32File) asyncIo(c *ioOperation, d *deadlineHandler, bytes uint32, er // code to ioCompletionProcessor, c must remain alive // until the channel read is complete. runtime.KeepAlive(c) - f.wg.Done() return int(r.bytes), err } @@ -202,6 +201,7 @@ func (f *win32File) Read(b []byte) (int, error) { if err != nil { return 0, err } + defer f.wg.Done() if f.readDeadline.timedout.isSet() { return 0, ErrTimeout @@ -228,6 +228,8 @@ func (f *win32File) Write(b []byte) (int, error) { if err != nil { return 0, err } + defer f.wg.Done() + if f.writeDeadline.timedout.isSet() { return 0, ErrTimeout } diff --git a/components/cli/vendor/github.com/Microsoft/go-winio/pipe.go b/components/cli/vendor/github.com/Microsoft/go-winio/pipe.go index 0ee2641ffc..fa270a310f 100644 --- a/components/cli/vendor/github.com/Microsoft/go-winio/pipe.go +++ b/components/cli/vendor/github.com/Microsoft/go-winio/pipe.go @@ -367,6 +367,8 @@ func connectPipe(p *win32File) error { if err != nil { return err } + defer p.wg.Done() + err = connectNamedPipe(p.handle, &c.o) _, err = p.asyncIo(c, nil, 0, err) if err != nil && err != cERROR_PIPE_CONNECTED { diff --git a/components/cli/vendor/github.com/cpuguy83/go-md2man/md2man.go b/components/cli/vendor/github.com/cpuguy83/go-md2man/md2man.go new file mode 100644 index 0000000000..8f6dcdaedf --- /dev/null +++ b/components/cli/vendor/github.com/cpuguy83/go-md2man/md2man.go @@ -0,0 +1,51 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + + "github.com/cpuguy83/go-md2man/md2man" +) + +var inFilePath = flag.String("in", "", "Path to file to be processed (default: stdin)") +var outFilePath = flag.String("out", "", "Path to output processed file (default: stdout)") + +func main() { + var err error + flag.Parse() + + inFile := os.Stdin + if *inFilePath != "" { + inFile, err = os.Open(*inFilePath) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + } + defer inFile.Close() + + doc, err := ioutil.ReadAll(inFile) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + out := md2man.Render(doc) + + outFile := os.Stdout + if *outFilePath != "" { + outFile, err = os.Create(*outFilePath) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + defer outFile.Close() + } + _, err = outFile.Write(out) + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/components/cli/vendor/github.com/docker/docker-credential-helpers/client/client.go b/components/cli/vendor/github.com/docker/docker-credential-helpers/client/client.go index d0813965b5..d1d0434cb5 100644 --- a/components/cli/vendor/github.com/docker/docker-credential-helpers/client/client.go +++ b/components/cli/vendor/github.com/docker/docker-credential-helpers/client/client.go @@ -9,12 +9,27 @@ import ( "github.com/docker/docker-credential-helpers/credentials" ) +// isValidCredsMessage checks if 'msg' contains invalid credentials error message. +// It returns whether the logs are free of invalid credentials errors and the error if it isn't. +// error values can be errCredentialsMissingServerURL or errCredentialsMissingUsername. +func isValidCredsMessage(msg string) error { + if credentials.IsCredentialsMissingServerURLMessage(msg) { + return credentials.NewErrCredentialsMissingServerURL() + } + + if credentials.IsCredentialsMissingUsernameMessage(msg) { + return credentials.NewErrCredentialsMissingUsername() + } + + return nil +} + // Store uses an external program to save credentials. -func Store(program ProgramFunc, credentials *credentials.Credentials) error { +func Store(program ProgramFunc, creds *credentials.Credentials) error { cmd := program("store") buffer := new(bytes.Buffer) - if err := json.NewEncoder(buffer).Encode(credentials); err != nil { + if err := json.NewEncoder(buffer).Encode(creds); err != nil { return err } cmd.Input(buffer) @@ -22,6 +37,11 @@ func Store(program ProgramFunc, credentials *credentials.Credentials) error { out, err := cmd.Output() if err != nil { t := strings.TrimSpace(string(out)) + + if isValidErr := isValidCredsMessage(t); isValidErr != nil { + err = isValidErr + } + return fmt.Errorf("error storing credentials - err: %v, out: `%s`", err, t) } @@ -41,6 +61,10 @@ func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error return nil, credentials.NewErrCredentialsNotFound() } + if isValidErr := isValidCredsMessage(t); isValidErr != nil { + err = isValidErr + } + return nil, fmt.Errorf("error getting credentials - err: %v, out: `%s`", err, t) } @@ -62,6 +86,11 @@ func Erase(program ProgramFunc, serverURL string) error { out, err := cmd.Output() if err != nil { t := strings.TrimSpace(string(out)) + + if isValidErr := isValidCredsMessage(t); isValidErr != nil { + err = isValidErr + } + return fmt.Errorf("error erasing credentials - err: %v, out: `%s`", err, t) } @@ -75,6 +104,11 @@ func List(program ProgramFunc) (map[string]string, error) { out, err := cmd.Output() if err != nil { t := strings.TrimSpace(string(out)) + + if isValidErr := isValidCredsMessage(t); isValidErr != nil { + err = isValidErr + } + return nil, fmt.Errorf("error listing credentials - err: %v, out: `%s`", err, t) } diff --git a/components/cli/vendor/github.com/docker/docker-credential-helpers/client/command.go b/components/cli/vendor/github.com/docker/docker-credential-helpers/client/command.go index 8983da6947..a144d5ac18 100644 --- a/components/cli/vendor/github.com/docker/docker-credential-helpers/client/command.go +++ b/components/cli/vendor/github.com/docker/docker-credential-helpers/client/command.go @@ -2,6 +2,7 @@ package client import ( "io" + "os" "os/exec" ) @@ -17,10 +18,16 @@ type ProgramFunc func(args ...string) Program // NewShellProgramFunc creates programs that are executed in a Shell. func NewShellProgramFunc(name string) ProgramFunc { return func(args ...string) Program { - return &Shell{cmd: exec.Command(name, args...)} + return &Shell{cmd: newCmdRedirectErr(name, args)} } } +func newCmdRedirectErr(name string, args []string) *exec.Cmd { + newCmd := exec.Command(name, args...) + newCmd.Stderr = os.Stderr + return newCmd +} + // Shell invokes shell commands to talk with a remote credentials helper. type Shell struct { cmd *exec.Cmd diff --git a/components/cli/vendor/github.com/docker/docker-credential-helpers/credentials/credentials.go b/components/cli/vendor/github.com/docker/docker-credential-helpers/credentials/credentials.go index cfe0cb134e..544ab3c4e1 100644 --- a/components/cli/vendor/github.com/docker/docker-credential-helpers/credentials/credentials.go +++ b/components/cli/vendor/github.com/docker/docker-credential-helpers/credentials/credentials.go @@ -17,6 +17,22 @@ type Credentials struct { Secret string } +// isValid checks the integrity of Credentials object such that no credentials lack +// a server URL or a username. +// It returns whether the credentials are valid and the error if it isn't. +// error values can be errCredentialsMissingServerURL or errCredentialsMissingUsername +func (c *Credentials) isValid() (bool, error) { + if len(c.ServerURL) == 0 { + return false, NewErrCredentialsMissingServerURL() + } + + if len(c.Username) == 0 { + return false, NewErrCredentialsMissingUsername() + } + + return true, nil +} + // Docker credentials should be labeled as such in credentials stores that allow labelling. // That label allows to filter out non-Docker credentials too at lookup/search in macOS keychain, // Windows credentials manager and Linux libsecret. Default value is "Docker Credentials" @@ -81,6 +97,10 @@ func Store(helper Helper, reader io.Reader) error { return err } + if ok, err := creds.isValid(); !ok { + return err + } + return helper.Add(&creds) } @@ -100,6 +120,9 @@ func Get(helper Helper, reader io.Reader, writer io.Writer) error { } serverURL := strings.TrimSpace(buffer.String()) + if len(serverURL) == 0 { + return NewErrCredentialsMissingServerURL() + } username, secret, err := helper.Get(serverURL) if err != nil { @@ -107,6 +130,7 @@ func Get(helper Helper, reader io.Reader, writer io.Writer) error { } resp := Credentials{ + ServerURL: serverURL, Username: username, Secret: secret, } @@ -135,6 +159,9 @@ func Erase(helper Helper, reader io.Reader) error { } serverURL := strings.TrimSpace(buffer.String()) + if len(serverURL) == 0 { + return NewErrCredentialsMissingServerURL() + } return helper.Delete(serverURL) } diff --git a/components/cli/vendor/github.com/docker/docker-credential-helpers/credentials/error.go b/components/cli/vendor/github.com/docker/docker-credential-helpers/credentials/error.go index d24bf16f09..588d4a83d7 100644 --- a/components/cli/vendor/github.com/docker/docker-credential-helpers/credentials/error.go +++ b/components/cli/vendor/github.com/docker/docker-credential-helpers/credentials/error.go @@ -1,8 +1,15 @@ package credentials -// ErrCredentialsNotFound standarizes the not found error, so every helper returns -// the same message and docker can handle it properly. -const errCredentialsNotFoundMessage = "credentials not found in native keychain" +const ( + // ErrCredentialsNotFound standardizes the not found error, so every helper returns + // the same message and docker can handle it properly. + errCredentialsNotFoundMessage = "credentials not found in native keychain" + + // ErrCredentialsMissingServerURL and ErrCredentialsMissingUsername standardize + // invalid credentials or credentials management operations + errCredentialsMissingServerURLMessage = "no credentials server URL" + errCredentialsMissingUsernameMessage = "no credentials username" +) // errCredentialsNotFound represents an error // raised when credentials are not in the store. @@ -35,3 +42,64 @@ func IsErrCredentialsNotFound(err error) bool { func IsErrCredentialsNotFoundMessage(err string) bool { return err == errCredentialsNotFoundMessage } + + +// errCredentialsMissingServerURL represents an error raised +// when the credentials object has no server URL or when no +// server URL is provided to a credentials operation requiring +// one. +type errCredentialsMissingServerURL struct{} + +func (errCredentialsMissingServerURL) Error() string { + return errCredentialsMissingServerURLMessage +} + +// errCredentialsMissingUsername represents an error raised +// when the credentials object has no username or when no +// username is provided to a credentials operation requiring +// one. +type errCredentialsMissingUsername struct{} + +func (errCredentialsMissingUsername) Error() string { + return errCredentialsMissingUsernameMessage +} + + +// NewErrCredentialsMissingServerURL creates a new error for +// errCredentialsMissingServerURL. +func NewErrCredentialsMissingServerURL() error { + return errCredentialsMissingServerURL{} +} + +// NewErrCredentialsMissingUsername creates a new error for +// errCredentialsMissingUsername. +func NewErrCredentialsMissingUsername() error { + return errCredentialsMissingUsername{} +} + + +// IsCredentialsMissingServerURL returns true if the error +// was an errCredentialsMissingServerURL. +func IsCredentialsMissingServerURL(err error) bool { + _, ok := err.(errCredentialsMissingServerURL) + return ok +} + +// IsCredentialsMissingServerURLMessage checks for an +// errCredentialsMissingServerURL in the error message. +func IsCredentialsMissingServerURLMessage(err string) bool { + return err == errCredentialsMissingServerURLMessage +} + +// IsCredentialsMissingUsername returns true if the error +// was an errCredentialsMissingUsername. +func IsCredentialsMissingUsername(err error) bool { + _, ok := err.(errCredentialsMissingUsername) + return ok +} + +// IsCredentialsMissingUsernameMessage checks for an +// errCredentialsMissingUsername in the error message. +func IsCredentialsMissingUsernameMessage(err string) bool { + return err == errCredentialsMissingUsernameMessage +} diff --git a/components/cli/vendor/github.com/docker/docker/api/common.go b/components/cli/vendor/github.com/docker/docker/api/common.go index 9df6255c08..142b76966c 100644 --- a/components/cli/vendor/github.com/docker/docker/api/common.go +++ b/components/cli/vendor/github.com/docker/docker/api/common.go @@ -21,7 +21,7 @@ import ( // Common constants for daemon and client. const ( // DefaultVersion of Current REST API - DefaultVersion string = "1.30" + DefaultVersion string = "1.31" // NoBaseImageSpecifier is the symbol used by the FROM // command to specify that no base image is to be used. diff --git a/components/cli/vendor/github.com/docker/docker/api/types/swarm/task.go b/components/cli/vendor/github.com/docker/docker/api/types/swarm/task.go index 9fd52e47d0..a598a79d59 100644 --- a/components/cli/vendor/github.com/docker/docker/api/types/swarm/task.go +++ b/components/cli/vendor/github.com/docker/docker/api/types/swarm/task.go @@ -67,9 +67,6 @@ type TaskSpec struct { ForceUpdate uint64 Runtime RuntimeType `json:",omitempty"` - // TODO (ehazlett): this should be removed and instead - // use struct tags (proto) for the runtimes - RuntimeData []byte `json:",omitempty"` } // Resources represents resources (CPU/Memory). diff --git a/components/cli/vendor/github.com/docker/docker/api/types/types.go b/components/cli/vendor/github.com/docker/docker/api/types/types.go index fa9f1b0de1..37adca2f57 100644 --- a/components/cli/vendor/github.com/docker/docker/api/types/types.go +++ b/components/cli/vendor/github.com/docker/docker/api/types/types.go @@ -277,7 +277,7 @@ type Health struct { // ContainerState stores container's running state // it's part of ContainerJSONBase and will return by "inspect" command type ContainerState struct { - Status string + Status string // String representation of the container state. Can be one of "created", "running", "paused", "restarting", "removing", "exited", or "dead" Running bool Paused bool Restarting bool @@ -468,6 +468,12 @@ type NetworkDisconnect struct { Force bool } +// NetworkInspectOptions holds parameters to inspect network +type NetworkInspectOptions struct { + Scope string + Verbose bool +} + // Checkpoint represents the details of a checkpoint type Checkpoint struct { Name string // Name is the name of the checkpoint diff --git a/components/cli/vendor/github.com/docker/docker/api/types/volume.go b/components/cli/vendor/github.com/docker/docker/api/types/volume.go index da4f8ebd9c..a69b0cfb17 100644 --- a/components/cli/vendor/github.com/docker/docker/api/types/volume.go +++ b/components/cli/vendor/github.com/docker/docker/api/types/volume.go @@ -7,6 +7,9 @@ package types // swagger:model Volume type Volume struct { + // Time volume was created. + CreatedAt string `json:"CreatedAt,omitempty"` + // Name of the volume driver used by the volume. // Required: true Driver string `json:"Driver"` diff --git a/components/cli/vendor/github.com/docker/docker/pkg/gitutils/gitutils.go b/components/cli/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go similarity index 99% rename from components/cli/vendor/github.com/docker/docker/pkg/gitutils/gitutils.go rename to components/cli/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go index 4e09e05239..fd8250dbc3 100644 --- a/components/cli/vendor/github.com/docker/docker/pkg/gitutils/gitutils.go +++ b/components/cli/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go @@ -1,4 +1,4 @@ -package gitutils +package git import ( "fmt" diff --git a/components/cli/vendor/github.com/docker/docker/client/client.go b/components/cli/vendor/github.com/docker/docker/client/client.go index f8f2fc6ad5..10fd73759b 100644 --- a/components/cli/vendor/github.com/docker/docker/client/client.go +++ b/components/cli/vendor/github.com/docker/docker/client/client.go @@ -216,9 +216,9 @@ func (cli *Client) getAPIPath(p string, query url.Values) string { var apiPath string if cli.version != "" { v := strings.TrimPrefix(cli.version, "v") - apiPath = fmt.Sprintf("%s/v%s%s", cli.basePath, v, p) + apiPath = cli.basePath + "/v" + v + p } else { - apiPath = fmt.Sprintf("%s%s", cli.basePath, p) + apiPath = cli.basePath + p } u := &url.URL{ @@ -247,6 +247,12 @@ func (cli *Client) UpdateClientVersion(v string) { } +// DaemonHost returns the host associated with this instance of the Client. +// This operation doesn't acquire a mutex. +func (cli *Client) DaemonHost() string { + return cli.host +} + // ParseHost verifies that the given host strings is valid. func ParseHost(host string) (string, string, string, error) { protoAddrParts := strings.SplitN(host, "://", 2) diff --git a/components/cli/vendor/github.com/docker/docker/client/container_copy.go b/components/cli/vendor/github.com/docker/docker/client/container_copy.go index 545aa54383..66ee5c1b3b 100644 --- a/components/cli/vendor/github.com/docker/docker/client/container_copy.go +++ b/components/cli/vendor/github.com/docker/docker/client/container_copy.go @@ -20,7 +20,7 @@ func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path stri query := url.Values{} query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API. - urlStr := fmt.Sprintf("/containers/%s/archive", containerID) + urlStr := "/containers/" + containerID + "/archive" response, err := cli.head(ctx, urlStr, query, nil) if err != nil { return types.ContainerPathStat{}, err @@ -42,7 +42,7 @@ func (cli *Client) CopyToContainer(ctx context.Context, container, path string, query.Set("copyUIDGID", "true") } - apiPath := fmt.Sprintf("/containers/%s/archive", container) + apiPath := "/containers/" + container + "/archive" response, err := cli.putRaw(ctx, apiPath, query, content, nil) if err != nil { @@ -63,7 +63,7 @@ func (cli *Client) CopyFromContainer(ctx context.Context, container, srcPath str query := make(url.Values, 1) query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API. - apiPath := fmt.Sprintf("/containers/%s/archive", container) + apiPath := "/containers/" + container + "/archive" response, err := cli.get(ctx, apiPath, query, nil) if err != nil { return nil, types.ContainerPathStat{}, err diff --git a/components/cli/vendor/github.com/docker/docker/client/container_wait.go b/components/cli/vendor/github.com/docker/docker/client/container_wait.go index edfa5d3c85..13b5eea6a9 100644 --- a/components/cli/vendor/github.com/docker/docker/client/container_wait.go +++ b/components/cli/vendor/github.com/docker/docker/client/container_wait.go @@ -10,7 +10,7 @@ import ( "github.com/docker/docker/api/types/versions" ) -// ContainerWait waits until the specified continer is in a certain state +// ContainerWait waits until the specified container is in a certain state // indicated by the given condition, either "not-running" (default), // "next-exit", or "removed". // @@ -31,7 +31,7 @@ func (cli *Client) ContainerWait(ctx context.Context, containerID string, condit } resultC := make(chan container.ContainerWaitOKBody) - errC := make(chan error) + errC := make(chan error, 1) query := url.Values{} query.Set("condition", string(condition)) diff --git a/components/cli/vendor/github.com/docker/docker/client/hijack.go b/components/cli/vendor/github.com/docker/docker/client/hijack.go index 74c53f52b3..2b61a80950 100644 --- a/components/cli/vendor/github.com/docker/docker/client/hijack.go +++ b/components/cli/vendor/github.com/docker/docker/client/hijack.go @@ -1,9 +1,11 @@ package client import ( + "bytes" "crypto/tls" "errors" "fmt" + "io/ioutil" "net" "net/http" "net/http/httputil" @@ -72,11 +74,23 @@ func (cli *Client) postHijacked(ctx context.Context, path string, query url.Valu defer clientconn.Close() // Server hijacks the connection, error 'connection closed' expected - _, err = clientconn.Do(req) + resp, err := clientconn.Do(req) + if err != nil { + return types.HijackedResponse{}, err + } - rwc, br := clientconn.Hijack() + defer resp.Body.Close() + switch resp.StatusCode { + case http.StatusOK, http.StatusSwitchingProtocols: + rwc, br := clientconn.Hijack() + return types.HijackedResponse{Conn: rwc, Reader: br}, err + } - return types.HijackedResponse{Conn: rwc, Reader: br}, err + errbody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return types.HijackedResponse{}, err + } + return types.HijackedResponse{}, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(errbody)) } func tlsDial(network, addr string, config *tls.Config) (net.Conn, error) { diff --git a/components/cli/vendor/github.com/docker/docker/client/interface.go b/components/cli/vendor/github.com/docker/docker/client/interface.go index 678a69ddf7..aa00622127 100644 --- a/components/cli/vendor/github.com/docker/docker/client/interface.go +++ b/components/cli/vendor/github.com/docker/docker/client/interface.go @@ -31,6 +31,7 @@ type CommonAPIClient interface { SystemAPIClient VolumeAPIClient ClientVersion() string + DaemonHost() string ServerVersion(ctx context.Context) (types.Version, error) UpdateClientVersion(v string) } @@ -98,8 +99,8 @@ type NetworkAPIClient interface { NetworkConnect(ctx context.Context, networkID, container string, config *network.EndpointSettings) error NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) NetworkDisconnect(ctx context.Context, networkID, container string, force bool) error - NetworkInspect(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, error) - NetworkInspectWithRaw(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, []byte, error) + NetworkInspect(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) + NetworkInspectWithRaw(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, []byte, error) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) NetworkRemove(ctx context.Context, networkID string) error NetworksPrune(ctx context.Context, pruneFilter filters.Args) (types.NetworksPruneReport, error) diff --git a/components/cli/vendor/github.com/docker/docker/client/network_inspect.go b/components/cli/vendor/github.com/docker/docker/client/network_inspect.go index 7242304025..848c9799fb 100644 --- a/components/cli/vendor/github.com/docker/docker/client/network_inspect.go +++ b/components/cli/vendor/github.com/docker/docker/client/network_inspect.go @@ -12,22 +12,25 @@ import ( ) // NetworkInspect returns the information for a specific network configured in the docker host. -func (cli *Client) NetworkInspect(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, error) { - networkResource, _, err := cli.NetworkInspectWithRaw(ctx, networkID, verbose) +func (cli *Client) NetworkInspect(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) { + networkResource, _, err := cli.NetworkInspectWithRaw(ctx, networkID, options) return networkResource, err } // NetworkInspectWithRaw returns the information for a specific network configured in the docker host and its raw representation. -func (cli *Client) NetworkInspectWithRaw(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, []byte, error) { +func (cli *Client) NetworkInspectWithRaw(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, []byte, error) { var ( networkResource types.NetworkResource resp serverResponse err error ) query := url.Values{} - if verbose { + if options.Verbose { query.Set("verbose", "true") } + if options.Scope != "" { + query.Set("scope", options.Scope) + } resp, err = cli.get(ctx, "/networks/"+networkID, query, nil) if err != nil { if resp.statusCode == http.StatusNotFound { diff --git a/components/cli/vendor/github.com/docker/docker/client/ping.go b/components/cli/vendor/github.com/docker/docker/client/ping.go index d6212ef8bb..631ed02005 100644 --- a/components/cli/vendor/github.com/docker/docker/client/ping.go +++ b/components/cli/vendor/github.com/docker/docker/client/ping.go @@ -1,8 +1,6 @@ package client import ( - "fmt" - "github.com/docker/docker/api/types" "golang.org/x/net/context" ) @@ -10,7 +8,7 @@ import ( // Ping pings the server and returns the value of the "Docker-Experimental", "OS-Type" & "API-Version" headers func (cli *Client) Ping(ctx context.Context) (types.Ping, error) { var ping types.Ping - req, err := cli.buildRequest("GET", fmt.Sprintf("%s/_ping", cli.basePath), nil, nil) + req, err := cli.buildRequest("GET", cli.basePath+"/_ping", nil, nil) if err != nil { return ping, err } diff --git a/components/cli/vendor/github.com/docker/docker/client/plugin_upgrade.go b/components/cli/vendor/github.com/docker/docker/client/plugin_upgrade.go index 24293c5073..28415156cd 100644 --- a/components/cli/vendor/github.com/docker/docker/client/plugin_upgrade.go +++ b/components/cli/vendor/github.com/docker/docker/client/plugin_upgrade.go @@ -1,7 +1,6 @@ package client import ( - "fmt" "io" "net/url" @@ -33,5 +32,5 @@ func (cli *Client) PluginUpgrade(ctx context.Context, name string, options types func (cli *Client) tryPluginUpgrade(ctx context.Context, query url.Values, privileges types.PluginPrivileges, name, registryAuth string) (serverResponse, error) { headers := map[string][]string{"X-Registry-Auth": {registryAuth}} - return cli.post(ctx, fmt.Sprintf("/plugins/%s/upgrade", name), query, privileges, headers) + return cli.post(ctx, "/plugins/"+name+"/upgrade", query, privileges, headers) } diff --git a/components/cli/vendor/github.com/docker/docker/client/service_create.go b/components/cli/vendor/github.com/docker/docker/client/service_create.go index 73fc68fb33..71a7583e86 100644 --- a/components/cli/vendor/github.com/docker/docker/client/service_create.go +++ b/components/cli/vendor/github.com/docker/docker/client/service_create.go @@ -24,14 +24,18 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, headers["X-Registry-Auth"] = []string{options.EncodedRegistryAuth} } + // ensure that the image is tagged + if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" { + service.TaskTemplate.ContainerSpec.Image = taggedImg + } + // Contact the registry to retrieve digest and platform information if options.QueryRegistry { distributionInspect, err := cli.DistributionInspect(ctx, service.TaskTemplate.ContainerSpec.Image, options.EncodedRegistryAuth) distErr = err if err == nil { // now pin by digest if the image doesn't already contain a digest - img := imageWithDigestString(service.TaskTemplate.ContainerSpec.Image, distributionInspect.Descriptor.Digest) - if img != "" { + if img := imageWithDigestString(service.TaskTemplate.ContainerSpec.Image, distributionInspect.Descriptor.Digest); img != "" { service.TaskTemplate.ContainerSpec.Image = img } // add platforms that are compatible with the service @@ -55,25 +59,33 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, } // imageWithDigestString takes an image string and a digest, and updates -// the image string if it didn't originally contain a digest. It assumes -// that the image string is not an image ID +// the image string if it didn't originally contain a digest. It returns +// an empty string if there are no updates. func imageWithDigestString(image string, dgst digest.Digest) string { - isCanonical := false - ref, err := reference.ParseAnyReference(image) + namedRef, err := reference.ParseNormalizedNamed(image) if err == nil { - _, isCanonical = ref.(reference.Canonical) - - if !isCanonical { - namedRef, _ := ref.(reference.Named) + if _, isCanonical := namedRef.(reference.Canonical); !isCanonical { + // ensure that image gets a default tag if none is provided img, err := reference.WithDigest(namedRef, dgst) if err == nil { - return img.String() + return reference.FamiliarString(img) } } } return "" } +// imageWithTagString takes an image string, and returns a tagged image +// string, adding a 'latest' tag if one was not provided. It returns an +// emptry string if a canonical reference was provided +func imageWithTagString(image string) string { + namedRef, err := reference.ParseNormalizedNamed(image) + if err == nil { + return reference.FamiliarString(reference.TagNameOnly(namedRef)) + } + return "" +} + // updateServicePlatforms updates the Platforms in swarm.Placement to list // all compatible platforms for the service, as found in distributionInspect // and returns a pointer to the new or updated swarm.Placement struct diff --git a/components/cli/vendor/github.com/docker/docker/client/service_update.go b/components/cli/vendor/github.com/docker/docker/client/service_update.go index 0cb35a1991..412790b5dd 100644 --- a/components/cli/vendor/github.com/docker/docker/client/service_update.go +++ b/components/cli/vendor/github.com/docker/docker/client/service_update.go @@ -35,6 +35,11 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version query.Set("version", strconv.FormatUint(version.Index, 10)) + // ensure that the image is tagged + if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" { + service.TaskTemplate.ContainerSpec.Image = taggedImg + } + // Contact the registry to retrieve digest and platform information // This happens only when the image has changed if options.QueryRegistry { @@ -42,8 +47,7 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version distErr = err if err == nil { // now pin by digest if the image doesn't already contain a digest - img := imageWithDigestString(service.TaskTemplate.ContainerSpec.Image, distributionInspect.Descriptor.Digest) - if img != "" { + if img := imageWithDigestString(service.TaskTemplate.ContainerSpec.Image, distributionInspect.Descriptor.Digest); img != "" { service.TaskTemplate.ContainerSpec.Image = img } // add platforms that are compatible with the service diff --git a/components/cli/vendor/github.com/docker/docker/opts/config.go b/components/cli/vendor/github.com/docker/docker/opts/config.go deleted file mode 100644 index 82fd2bce4e..0000000000 --- a/components/cli/vendor/github.com/docker/docker/opts/config.go +++ /dev/null @@ -1,98 +0,0 @@ -package opts - -import ( - "encoding/csv" - "fmt" - "os" - "strconv" - "strings" - - swarmtypes "github.com/docker/docker/api/types/swarm" -) - -// ConfigOpt is a Value type for parsing configs -type ConfigOpt struct { - values []*swarmtypes.ConfigReference -} - -// Set a new config value -func (o *ConfigOpt) Set(value string) error { - csvReader := csv.NewReader(strings.NewReader(value)) - fields, err := csvReader.Read() - if err != nil { - return err - } - - options := &swarmtypes.ConfigReference{ - File: &swarmtypes.ConfigReferenceFileTarget{ - UID: "0", - GID: "0", - Mode: 0444, - }, - } - - // support a simple syntax of --config foo - if len(fields) == 1 { - options.File.Name = fields[0] - options.ConfigName = fields[0] - o.values = append(o.values, options) - return nil - } - - for _, field := range fields { - parts := strings.SplitN(field, "=", 2) - key := strings.ToLower(parts[0]) - - if len(parts) != 2 { - return fmt.Errorf("invalid field '%s' must be a key=value pair", field) - } - - value := parts[1] - switch key { - case "source", "src": - options.ConfigName = value - case "target": - options.File.Name = value - case "uid": - options.File.UID = value - case "gid": - options.File.GID = value - case "mode": - m, err := strconv.ParseUint(value, 0, 32) - if err != nil { - return fmt.Errorf("invalid mode specified: %v", err) - } - - options.File.Mode = os.FileMode(m) - default: - return fmt.Errorf("invalid field in config request: %s", key) - } - } - - if options.ConfigName == "" { - return fmt.Errorf("source is required") - } - - o.values = append(o.values, options) - return nil -} - -// Type returns the type of this option -func (o *ConfigOpt) Type() string { - return "config" -} - -// String returns a string repr of this option -func (o *ConfigOpt) String() string { - configs := []string{} - for _, config := range o.values { - repr := fmt.Sprintf("%s -> %s", config.ConfigName, config.File.Name) - configs = append(configs, repr) - } - return strings.Join(configs, ", ") -} - -// Value returns the config requests -func (o *ConfigOpt) Value() []*swarmtypes.ConfigReference { - return o.values -} diff --git a/components/cli/vendor/github.com/docker/docker/opts/opts.go b/components/cli/vendor/github.com/docker/docker/opts/opts.go index f76f308051..8d82f76792 100644 --- a/components/cli/vendor/github.com/docker/docker/opts/opts.go +++ b/components/cli/vendor/github.com/docker/docker/opts/opts.go @@ -2,13 +2,11 @@ package opts import ( "fmt" - "math/big" "net" "path" "regexp" "strings" - "github.com/docker/docker/api/types/filters" units "github.com/docker/go-units" ) @@ -236,15 +234,6 @@ func ValidateIPAddress(val string) (string, error) { return "", fmt.Errorf("%s is not an ip address", val) } -// ValidateMACAddress validates a MAC address. -func ValidateMACAddress(val string) (string, error) { - _, err := net.ParseMAC(strings.TrimSpace(val)) - if err != nil { - return "", err - } - return val, nil -} - // ValidateDNSSearch validates domain for resolvconf search configuration. // A zero length domain is represented by a dot (.). func ValidateDNSSearch(val string) (string, error) { @@ -274,114 +263,6 @@ func ValidateLabel(val string) (string, error) { return val, nil } -// ValidateSysctl validates a sysctl and returns it. -func ValidateSysctl(val string) (string, error) { - validSysctlMap := map[string]bool{ - "kernel.msgmax": true, - "kernel.msgmnb": true, - "kernel.msgmni": true, - "kernel.sem": true, - "kernel.shmall": true, - "kernel.shmmax": true, - "kernel.shmmni": true, - "kernel.shm_rmid_forced": true, - } - validSysctlPrefixes := []string{ - "net.", - "fs.mqueue.", - } - arr := strings.Split(val, "=") - if len(arr) < 2 { - return "", fmt.Errorf("sysctl '%s' is not whitelisted", val) - } - if validSysctlMap[arr[0]] { - return val, nil - } - - for _, vp := range validSysctlPrefixes { - if strings.HasPrefix(arr[0], vp) { - return val, nil - } - } - return "", fmt.Errorf("sysctl '%s' is not whitelisted", val) -} - -// FilterOpt is a flag type for validating filters -type FilterOpt struct { - filter filters.Args -} - -// NewFilterOpt returns a new FilterOpt -func NewFilterOpt() FilterOpt { - return FilterOpt{filter: filters.NewArgs()} -} - -func (o *FilterOpt) String() string { - repr, err := filters.ToParam(o.filter) - if err != nil { - return "invalid filters" - } - return repr -} - -// Set sets the value of the opt by parsing the command line value -func (o *FilterOpt) Set(value string) error { - var err error - o.filter, err = filters.ParseFlag(value, o.filter) - return err -} - -// Type returns the option type -func (o *FilterOpt) Type() string { - return "filter" -} - -// Value returns the value of this option -func (o *FilterOpt) Value() filters.Args { - return o.filter -} - -// NanoCPUs is a type for fixed point fractional number. -type NanoCPUs int64 - -// String returns the string format of the number -func (c *NanoCPUs) String() string { - if *c == 0 { - return "" - } - return big.NewRat(c.Value(), 1e9).FloatString(3) -} - -// Set sets the value of the NanoCPU by passing a string -func (c *NanoCPUs) Set(value string) error { - cpus, err := ParseCPUs(value) - *c = NanoCPUs(cpus) - return err -} - -// Type returns the type -func (c *NanoCPUs) Type() string { - return "decimal" -} - -// Value returns the value in int64 -func (c *NanoCPUs) Value() int64 { - return int64(*c) -} - -// ParseCPUs takes a string ratio and returns an integer value of nano cpus -func ParseCPUs(value string) (int64, error) { - cpu, ok := new(big.Rat).SetString(value) - if !ok { - return 0, fmt.Errorf("failed to parse %v as a rational number", value) - } - nano := cpu.Mul(cpu, big.NewRat(1e9, 1)) - if !nano.IsInt() { - return 0, fmt.Errorf("value is too precise") - } - return nano.Num().Int64(), nil -} - // ParseLink parses and validates the specified string as a link format (name:alias) func ParseLink(val string) (string, string, error) { if val == "" { @@ -404,12 +285,6 @@ func ParseLink(val string) (string, string, error) { return arr[0], arr[1], nil } -// ValidateLink validates that the specified string has a valid link format (containerName:alias). -func ValidateLink(val string) (string, error) { - _, _, err := ParseLink(val) - return val, err -} - // MemBytes is a type for human readable memory bytes (like 128M, 2g, etc) type MemBytes int64 @@ -450,39 +325,3 @@ func (m *MemBytes) UnmarshalJSON(s []byte) error { *m = MemBytes(val) return err } - -// MemSwapBytes is a type for human readable memory bytes (like 128M, 2g, etc). -// It differs from MemBytes in that -1 is valid and the default. -type MemSwapBytes int64 - -// Set sets the value of the MemSwapBytes by passing a string -func (m *MemSwapBytes) Set(value string) error { - if value == "-1" { - *m = MemSwapBytes(-1) - return nil - } - val, err := units.RAMInBytes(value) - *m = MemSwapBytes(val) - return err -} - -// Type returns the type -func (m *MemSwapBytes) Type() string { - return "bytes" -} - -// Value returns the value in int64 -func (m *MemSwapBytes) Value() int64 { - return int64(*m) -} - -func (m *MemSwapBytes) String() string { - b := MemBytes(*m) - return b.String() -} - -// UnmarshalJSON is the customized unmarshaler for MemSwapBytes -func (m *MemSwapBytes) UnmarshalJSON(s []byte) error { - b := MemBytes(*m) - return b.UnmarshalJSON(s) -} diff --git a/components/cli/vendor/github.com/docker/docker/opts/throttledevice.go b/components/cli/vendor/github.com/docker/docker/opts/throttledevice.go deleted file mode 100644 index 65dd3ebf68..0000000000 --- a/components/cli/vendor/github.com/docker/docker/opts/throttledevice.go +++ /dev/null @@ -1,111 +0,0 @@ -package opts - -import ( - "fmt" - "strconv" - "strings" - - "github.com/docker/docker/api/types/blkiodev" - "github.com/docker/go-units" -) - -// ValidatorThrottleFctType defines a validator function that returns a validated struct and/or an error. -type ValidatorThrottleFctType func(val string) (*blkiodev.ThrottleDevice, error) - -// ValidateThrottleBpsDevice validates that the specified string has a valid device-rate format. -func ValidateThrottleBpsDevice(val string) (*blkiodev.ThrottleDevice, error) { - split := strings.SplitN(val, ":", 2) - if len(split) != 2 { - return nil, fmt.Errorf("bad format: %s", val) - } - if !strings.HasPrefix(split[0], "/dev/") { - return nil, fmt.Errorf("bad format for device path: %s", val) - } - rate, err := units.RAMInBytes(split[1]) - if err != nil { - return nil, fmt.Errorf("invalid rate for device: %s. The correct format is :[]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", val) - } - if rate < 0 { - return nil, fmt.Errorf("invalid rate for device: %s. The correct format is :[]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", val) - } - - return &blkiodev.ThrottleDevice{ - Path: split[0], - Rate: uint64(rate), - }, nil -} - -// ValidateThrottleIOpsDevice validates that the specified string has a valid device-rate format. -func ValidateThrottleIOpsDevice(val string) (*blkiodev.ThrottleDevice, error) { - split := strings.SplitN(val, ":", 2) - if len(split) != 2 { - return nil, fmt.Errorf("bad format: %s", val) - } - if !strings.HasPrefix(split[0], "/dev/") { - return nil, fmt.Errorf("bad format for device path: %s", val) - } - rate, err := strconv.ParseUint(split[1], 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid rate for device: %s. The correct format is :. Number must be a positive integer", val) - } - if rate < 0 { - return nil, fmt.Errorf("invalid rate for device: %s. The correct format is :. Number must be a positive integer", val) - } - - return &blkiodev.ThrottleDevice{ - Path: split[0], - Rate: uint64(rate), - }, nil -} - -// ThrottledeviceOpt defines a map of ThrottleDevices -type ThrottledeviceOpt struct { - values []*blkiodev.ThrottleDevice - validator ValidatorThrottleFctType -} - -// NewThrottledeviceOpt creates a new ThrottledeviceOpt -func NewThrottledeviceOpt(validator ValidatorThrottleFctType) ThrottledeviceOpt { - values := []*blkiodev.ThrottleDevice{} - return ThrottledeviceOpt{ - values: values, - validator: validator, - } -} - -// Set validates a ThrottleDevice and sets its name as a key in ThrottledeviceOpt -func (opt *ThrottledeviceOpt) Set(val string) error { - var value *blkiodev.ThrottleDevice - if opt.validator != nil { - v, err := opt.validator(val) - if err != nil { - return err - } - value = v - } - (opt.values) = append((opt.values), value) - return nil -} - -// String returns ThrottledeviceOpt values as a string. -func (opt *ThrottledeviceOpt) String() string { - var out []string - for _, v := range opt.values { - out = append(out, v.String()) - } - - return fmt.Sprintf("%v", out) -} - -// GetList returns a slice of pointers to ThrottleDevices. -func (opt *ThrottledeviceOpt) GetList() []*blkiodev.ThrottleDevice { - var throttledevice []*blkiodev.ThrottleDevice - throttledevice = append(throttledevice, opt.values...) - - return throttledevice -} - -// Type returns the option type -func (opt *ThrottledeviceOpt) Type() string { - return "list" -} diff --git a/components/cli/vendor/github.com/docker/docker/pkg/archive/archive.go b/components/cli/vendor/github.com/docker/docker/pkg/archive/archive.go index 30b3c5b36f..5fb774d602 100644 --- a/components/cli/vendor/github.com/docker/docker/pkg/archive/archive.go +++ b/components/cli/vendor/github.com/docker/docker/pkg/archive/archive.go @@ -6,7 +6,6 @@ import ( "bytes" "compress/bzip2" "compress/gzip" - "errors" "fmt" "io" "io/ioutil" @@ -31,10 +30,6 @@ type ( Compression int // WhiteoutFormat is the format of whiteouts unpacked WhiteoutFormat int - // TarChownOptions wraps the chown options UID and GID. - TarChownOptions struct { - UID, GID int - } // TarOptions wraps the tar options. TarOptions struct { @@ -44,7 +39,7 @@ type ( NoLchown bool UIDMaps []idtools.IDMap GIDMaps []idtools.IDMap - ChownOpts *TarChownOptions + ChownOpts *idtools.IDPair IncludeSourceDir bool // WhiteoutFormat is the expected on disk format for whiteout files. // This format will be converted to the standard format on pack @@ -58,33 +53,26 @@ type ( RebaseNames map[string]string InUserNS bool } - - // Archiver allows the reuse of most utility functions of this package - // with a pluggable Untar function. Also, to facilitate the passing of - // specific id mappings for untar, an archiver can be created with maps - // which will then be passed to Untar operations - Archiver struct { - Untar func(io.Reader, string, *TarOptions) error - UIDMaps []idtools.IDMap - GIDMaps []idtools.IDMap - } - - // breakoutError is used to differentiate errors related to breaking out - // When testing archive breakout in the unit tests, this error is expected - // in order for the test to pass. - breakoutError error ) -var ( - // ErrNotImplemented is the error message of function not implemented. - ErrNotImplemented = errors.New("Function not implemented") - defaultArchiver = &Archiver{Untar: Untar, UIDMaps: nil, GIDMaps: nil} -) +// Archiver allows the reuse of most utility functions of this package +// with a pluggable Untar function. Also, to facilitate the passing of +// specific id mappings for untar, an archiver can be created with maps +// which will then be passed to Untar operations +type Archiver struct { + Untar func(io.Reader, string, *TarOptions) error + IDMappings *idtools.IDMappings +} -const ( - // HeaderSize is the size in bytes of a tar header - HeaderSize = 512 -) +// NewDefaultArchiver returns a new Archiver without any IDMappings +func NewDefaultArchiver() *Archiver { + return &Archiver{Untar: Untar, IDMappings: &idtools.IDMappings{}} +} + +// breakoutError is used to differentiate errors related to breaking out +// When testing archive breakout in the unit tests, this error is expected +// in order for the test to pass. +type breakoutError error const ( // Uncompressed represents the uncompressed. @@ -105,18 +93,6 @@ const ( OverlayWhiteoutFormat ) -// IsArchive checks for the magic bytes of a tar or any supported compression -// algorithm. -func IsArchive(header []byte) bool { - compression := DetectCompression(header) - if compression != Uncompressed { - return true - } - r := tar.NewReader(bytes.NewBuffer(header)) - _, err := r.Next() - return err == nil -} - // IsArchivePath checks if the (possibly compressed) file at the given path // starts with a tar file header. func IsArchivePath(path string) bool { @@ -369,9 +345,8 @@ type tarAppender struct { Buffer *bufio.Writer // for hardlink mapping - SeenFiles map[uint64]string - UIDMaps []idtools.IDMap - GIDMaps []idtools.IDMap + SeenFiles map[uint64]string + IDMappings *idtools.IDMappings // For packing and unpacking whiteout files in the // non standard format. The whiteout files defined @@ -380,6 +355,15 @@ type tarAppender struct { WhiteoutConverter tarWhiteoutConverter } +func newTarAppender(idMapping *idtools.IDMappings, writer io.Writer) *tarAppender { + return &tarAppender{ + SeenFiles: make(map[uint64]string), + TarWriter: tar.NewWriter(writer), + Buffer: pools.BufioWriter32KPool.Get(nil), + IDMappings: idMapping, + } +} + // canonicalTarName provides a platform-independent and consistent posix-style //path for files and directories to be archived regardless of the platform. func canonicalTarName(name string, isDir bool) (string, error) { @@ -428,21 +412,15 @@ func (ta *tarAppender) addTarFile(path, name string) error { //handle re-mapping container ID mappings back to host ID mappings before //writing tar headers/files. We skip whiteout files because they were written //by the kernel and already have proper ownership relative to the host - if !strings.HasPrefix(filepath.Base(hdr.Name), WhiteoutPrefix) && (ta.UIDMaps != nil || ta.GIDMaps != nil) { - uid, gid, err := getFileUIDGID(fi.Sys()) + if !strings.HasPrefix(filepath.Base(hdr.Name), WhiteoutPrefix) && !ta.IDMappings.Empty() { + fileIDPair, err := getFileUIDGID(fi.Sys()) if err != nil { return err } - xUID, err := idtools.ToContainer(uid, ta.UIDMaps) + hdr.Uid, hdr.Gid, err = ta.IDMappings.ToContainer(fileIDPair) if err != nil { return err } - xGID, err := idtools.ToContainer(gid, ta.GIDMaps) - if err != nil { - return err - } - hdr.Uid = xUID - hdr.Gid = xGID } if ta.WhiteoutConverter != nil { @@ -496,7 +474,7 @@ func (ta *tarAppender) addTarFile(path, name string) error { return nil } -func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, Lchown bool, chownOpts *TarChownOptions, inUserns bool) error { +func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, Lchown bool, chownOpts *idtools.IDPair, inUserns bool) error { // hdr.Mode is in linux format, which we can use for sycalls, // but for os.Foo() calls we need the mode converted to os.FileMode, // so use hdrInfo.Mode() (they differ for e.g. setuid bits) @@ -576,7 +554,7 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, L // Lchown is not supported on Windows. if Lchown && runtime.GOOS != "windows" { if chownOpts == nil { - chownOpts = &TarChownOptions{UID: hdr.Uid, GID: hdr.Gid} + chownOpts = &idtools.IDPair{UID: hdr.Uid, GID: hdr.Gid} } if err := os.Lchown(path, chownOpts.UID, chownOpts.GID); err != nil { return err @@ -664,14 +642,11 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) } go func() { - ta := &tarAppender{ - TarWriter: tar.NewWriter(compressWriter), - Buffer: pools.BufioWriter32KPool.Get(nil), - SeenFiles: make(map[uint64]string), - UIDMaps: options.UIDMaps, - GIDMaps: options.GIDMaps, - WhiteoutConverter: getWhiteoutConverter(options.WhiteoutFormat), - } + ta := newTarAppender( + idtools.NewIDMappingsFromMaps(options.UIDMaps, options.GIDMaps), + compressWriter, + ) + ta.WhiteoutConverter = getWhiteoutConverter(options.WhiteoutFormat) defer func() { // Make sure to check the error on Close. @@ -827,10 +802,8 @@ func Unpack(decompressedArchive io.Reader, dest string, options *TarOptions) err defer pools.BufioReader32KPool.Put(trBuf) var dirs []*tar.Header - remappedRootUID, remappedRootGID, err := idtools.GetRootUIDGID(options.UIDMaps, options.GIDMaps) - if err != nil { - return err - } + idMappings := idtools.NewIDMappingsFromMaps(options.UIDMaps, options.GIDMaps) + rootIDs := idMappings.RootPair() whiteoutConverter := getWhiteoutConverter(options.WhiteoutFormat) // Iterate through the files in the archive. @@ -864,7 +837,7 @@ loop: parent := filepath.Dir(hdr.Name) parentPath := filepath.Join(dest, parent) if _, err := os.Lstat(parentPath); err != nil && os.IsNotExist(err) { - err = idtools.MkdirAllNewAs(parentPath, 0777, remappedRootUID, remappedRootGID) + err = idtools.MkdirAllAndChownNew(parentPath, 0777, rootIDs) if err != nil { return err } @@ -909,26 +882,8 @@ loop: } trBuf.Reset(tr) - // if the options contain a uid & gid maps, convert header uid/gid - // entries using the maps such that lchown sets the proper mapped - // uid/gid after writing the file. We only perform this mapping if - // the file isn't already owned by the remapped root UID or GID, as - // that specific uid/gid has no mapping from container -> host, and - // those files already have the proper ownership for inside the - // container. - if hdr.Uid != remappedRootUID { - xUID, err := idtools.ToHost(hdr.Uid, options.UIDMaps) - if err != nil { - return err - } - hdr.Uid = xUID - } - if hdr.Gid != remappedRootGID { - xGID, err := idtools.ToHost(hdr.Gid, options.GIDMaps) - if err != nil { - return err - } - hdr.Gid = xGID + if err := remapIDs(idMappings, hdr); err != nil { + return err } if whiteoutConverter != nil { @@ -1013,23 +968,13 @@ func (archiver *Archiver) TarUntar(src, dst string) error { return err } defer archive.Close() - - var options *TarOptions - if archiver.UIDMaps != nil || archiver.GIDMaps != nil { - options = &TarOptions{ - UIDMaps: archiver.UIDMaps, - GIDMaps: archiver.GIDMaps, - } + options := &TarOptions{ + UIDMaps: archiver.IDMappings.UIDs(), + GIDMaps: archiver.IDMappings.GIDs(), } return archiver.Untar(archive, dst, options) } -// TarUntar is a convenience function which calls Tar and Untar, with the output of one piped into the other. -// If either Tar or Untar fails, TarUntar aborts and returns the error. -func TarUntar(src, dst string) error { - return defaultArchiver.TarUntar(src, dst) -} - // UntarPath untar a file from path to a destination, src is the source tar file path. func (archiver *Archiver) UntarPath(src, dst string) error { archive, err := os.Open(src) @@ -1037,22 +982,13 @@ func (archiver *Archiver) UntarPath(src, dst string) error { return err } defer archive.Close() - var options *TarOptions - if archiver.UIDMaps != nil || archiver.GIDMaps != nil { - options = &TarOptions{ - UIDMaps: archiver.UIDMaps, - GIDMaps: archiver.GIDMaps, - } + options := &TarOptions{ + UIDMaps: archiver.IDMappings.UIDs(), + GIDMaps: archiver.IDMappings.GIDs(), } return archiver.Untar(archive, dst, options) } -// UntarPath is a convenience function which looks for an archive -// at filesystem path `src`, and unpacks it at `dst`. -func UntarPath(src, dst string) error { - return defaultArchiver.UntarPath(src, dst) -} - // CopyWithTar creates a tar archive of filesystem path `src`, and // unpacks it at filesystem path `dst`. // The archive is streamed directly with fixed buffering and no @@ -1069,27 +1005,16 @@ func (archiver *Archiver) CopyWithTar(src, dst string) error { // if this archiver is set up with ID mapping we need to create // the new destination directory with the remapped root UID/GID pair // as owner - rootUID, rootGID, err := idtools.GetRootUIDGID(archiver.UIDMaps, archiver.GIDMaps) - if err != nil { - return err - } + rootIDs := archiver.IDMappings.RootPair() // Create dst, copy src's content into it logrus.Debugf("Creating dest directory: %s", dst) - if err := idtools.MkdirAllNewAs(dst, 0755, rootUID, rootGID); err != nil { + if err := idtools.MkdirAllAndChownNew(dst, 0755, rootIDs); err != nil { return err } logrus.Debugf("Calling TarUntar(%s, %s)", src, dst) return archiver.TarUntar(src, dst) } -// CopyWithTar creates a tar archive of filesystem path `src`, and -// unpacks it at filesystem path `dst`. -// The archive is streamed directly with fixed buffering and no -// intermediary disk IO. -func CopyWithTar(src, dst string) error { - return defaultArchiver.CopyWithTar(src, dst) -} - // CopyFileWithTar emulates the behavior of the 'cp' command-line // for a single file. It copies a regular file from path `src` to // path `dst`, and preserves all its metadata. @@ -1131,28 +1056,10 @@ func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) { hdr.Name = filepath.Base(dst) hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode))) - remappedRootUID, remappedRootGID, err := idtools.GetRootUIDGID(archiver.UIDMaps, archiver.GIDMaps) - if err != nil { + if err := remapIDs(archiver.IDMappings, hdr); err != nil { return err } - // only perform mapping if the file being copied isn't already owned by the - // uid or gid of the remapped root in the container - if remappedRootUID != hdr.Uid { - xUID, err := idtools.ToHost(hdr.Uid, archiver.UIDMaps) - if err != nil { - return err - } - hdr.Uid = xUID - } - if remappedRootGID != hdr.Gid { - xGID, err := idtools.ToHost(hdr.Gid, archiver.GIDMaps) - if err != nil { - return err - } - hdr.Gid = xGID - } - tw := tar.NewWriter(w) defer tw.Close() if err := tw.WriteHeader(hdr); err != nil { @@ -1176,16 +1083,10 @@ func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) { return err } -// CopyFileWithTar emulates the behavior of the 'cp' command-line -// for a single file. It copies a regular file from path `src` to -// path `dst`, and preserves all its metadata. -// -// Destination handling is in an operating specific manner depending -// where the daemon is running. If `dst` ends with a trailing slash -// the final destination path will be `dst/base(src)` (Linux) or -// `dst\base(src)` (Windows). -func CopyFileWithTar(src, dst string) (err error) { - return defaultArchiver.CopyFileWithTar(src, dst) +func remapIDs(idMappings *idtools.IDMappings, hdr *tar.Header) error { + ids, err := idMappings.ToHost(idtools.IDPair{UID: hdr.Uid, GID: hdr.Gid}) + hdr.Uid, hdr.Gid = ids.UID, ids.GID + return err } // cmdStream executes a command, and returns its stdout as a stream. diff --git a/components/cli/vendor/github.com/docker/docker/pkg/archive/archive_unix.go b/components/cli/vendor/github.com/docker/docker/pkg/archive/archive_unix.go index 68d3c97d27..33ee88766f 100644 --- a/components/cli/vendor/github.com/docker/docker/pkg/archive/archive_unix.go +++ b/components/cli/vendor/github.com/docker/docker/pkg/archive/archive_unix.go @@ -9,6 +9,7 @@ import ( "path/filepath" "syscall" + "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/system" rsystem "github.com/opencontainers/runc/libcontainer/system" ) @@ -72,13 +73,13 @@ func getInodeFromStat(stat interface{}) (inode uint64, err error) { return } -func getFileUIDGID(stat interface{}) (int, int, error) { +func getFileUIDGID(stat interface{}) (idtools.IDPair, error) { s, ok := stat.(*syscall.Stat_t) if !ok { - return -1, -1, errors.New("cannot convert stat value to syscall.Stat_t") + return idtools.IDPair{}, errors.New("cannot convert stat value to syscall.Stat_t") } - return int(s.Uid), int(s.Gid), nil + return idtools.IDPair{UID: int(s.Uid), GID: int(s.Gid)}, nil } func major(device uint64) uint64 { diff --git a/components/cli/vendor/github.com/docker/docker/pkg/archive/archive_windows.go b/components/cli/vendor/github.com/docker/docker/pkg/archive/archive_windows.go index 9c7094738d..a22410c039 100644 --- a/components/cli/vendor/github.com/docker/docker/pkg/archive/archive_windows.go +++ b/components/cli/vendor/github.com/docker/docker/pkg/archive/archive_windows.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" + "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/longpath" ) @@ -72,7 +73,7 @@ func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error { return nil } -func getFileUIDGID(stat interface{}) (int, int, error) { +func getFileUIDGID(stat interface{}) (idtools.IDPair, error) { // no notion of file ownership mapping yet on Windows - return 0, 0, nil + return idtools.IDPair{0, 0}, nil } diff --git a/components/cli/vendor/github.com/docker/docker/pkg/archive/changes.go b/components/cli/vendor/github.com/docker/docker/pkg/archive/changes.go index ca2c0ca1bf..5ca39b7215 100644 --- a/components/cli/vendor/github.com/docker/docker/pkg/archive/changes.go +++ b/components/cli/vendor/github.com/docker/docker/pkg/archive/changes.go @@ -394,13 +394,8 @@ func ChangesSize(newDir string, changes []Change) int64 { func ExportChanges(dir string, changes []Change, uidMaps, gidMaps []idtools.IDMap) (io.ReadCloser, error) { reader, writer := io.Pipe() go func() { - ta := &tarAppender{ - TarWriter: tar.NewWriter(writer), - Buffer: pools.BufioWriter32KPool.Get(nil), - SeenFiles: make(map[uint64]string), - UIDMaps: uidMaps, - GIDMaps: gidMaps, - } + ta := newTarAppender(idtools.NewIDMappingsFromMaps(uidMaps, gidMaps), writer) + // this buffer is needed for the duration of this piped stream defer pools.BufioWriter32KPool.Put(ta.Buffer) diff --git a/components/cli/vendor/github.com/docker/docker/pkg/archive/diff.go b/components/cli/vendor/github.com/docker/docker/pkg/archive/diff.go index 2292193942..d20854e2a2 100644 --- a/components/cli/vendor/github.com/docker/docker/pkg/archive/diff.go +++ b/components/cli/vendor/github.com/docker/docker/pkg/archive/diff.go @@ -33,10 +33,7 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64, if options.ExcludePatterns == nil { options.ExcludePatterns = []string{} } - remappedRootUID, remappedRootGID, err := idtools.GetRootUIDGID(options.UIDMaps, options.GIDMaps) - if err != nil { - return 0, err - } + idMappings := idtools.NewIDMappingsFromMaps(options.UIDMaps, options.GIDMaps) aufsTempdir := "" aufsHardlinks := make(map[string]*tar.Header) @@ -195,27 +192,10 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64, srcData = tmpFile } - // if the options contain a uid & gid maps, convert header uid/gid - // entries using the maps such that lchown sets the proper mapped - // uid/gid after writing the file. We only perform this mapping if - // the file isn't already owned by the remapped root UID or GID, as - // that specific uid/gid has no mapping from container -> host, and - // those files already have the proper ownership for inside the - // container. - if srcHdr.Uid != remappedRootUID { - xUID, err := idtools.ToHost(srcHdr.Uid, options.UIDMaps) - if err != nil { - return 0, err - } - srcHdr.Uid = xUID - } - if srcHdr.Gid != remappedRootGID { - xGID, err := idtools.ToHost(srcHdr.Gid, options.GIDMaps) - if err != nil { - return 0, err - } - srcHdr.Gid = xGID + if err := remapIDs(idMappings, srcHdr); err != nil { + return 0, err } + if err := createTarFile(path, dest, srcHdr, srcData, true, nil, options.InUserNS); err != nil { return 0, err } diff --git a/components/cli/vendor/github.com/docker/docker/pkg/httputils/httputils.go b/components/cli/vendor/github.com/docker/docker/pkg/httputils/httputils.go deleted file mode 100644 index af86835bd9..0000000000 --- a/components/cli/vendor/github.com/docker/docker/pkg/httputils/httputils.go +++ /dev/null @@ -1,56 +0,0 @@ -package httputils - -import ( - "errors" - "fmt" - "net/http" - "regexp" - "strings" - - "github.com/docker/docker/pkg/jsonmessage" -) - -var ( - headerRegexp = regexp.MustCompile(`^(?:(.+)/(.+?))\((.+)\).*$`) - errInvalidHeader = errors.New("Bad header, should be in format `docker/version (platform)`") -) - -// Download requests a given URL and returns an io.Reader. -func Download(url string) (resp *http.Response, err error) { - if resp, err = http.Get(url); err != nil { - return nil, err - } - if resp.StatusCode >= 400 { - return nil, fmt.Errorf("Got HTTP status code >= 400: %s", resp.Status) - } - return resp, nil -} - -// NewHTTPRequestError returns a JSON response error. -func NewHTTPRequestError(msg string, res *http.Response) error { - return &jsonmessage.JSONError{ - Message: msg, - Code: res.StatusCode, - } -} - -// ServerHeader contains the server information. -type ServerHeader struct { - App string // docker - Ver string // 1.8.0-dev - OS string // windows or linux -} - -// ParseServerHeader extracts pieces from an HTTP server header -// which is in the format "docker/version (os)" e.g. docker/1.8.0-dev (windows). -func ParseServerHeader(hdr string) (*ServerHeader, error) { - matches := headerRegexp.FindStringSubmatch(hdr) - if len(matches) != 4 { - return nil, errInvalidHeader - } - return &ServerHeader{ - App: strings.TrimSpace(matches[1]), - Ver: strings.TrimSpace(matches[2]), - OS: strings.TrimSpace(matches[3]), - }, nil -} diff --git a/components/cli/vendor/github.com/docker/docker/pkg/httputils/mimetype.go b/components/cli/vendor/github.com/docker/docker/pkg/httputils/mimetype.go deleted file mode 100644 index abef9e9e83..0000000000 --- a/components/cli/vendor/github.com/docker/docker/pkg/httputils/mimetype.go +++ /dev/null @@ -1,29 +0,0 @@ -package httputils - -import ( - "mime" - "net/http" -) - -// MimeTypes stores the MIME content type. -var MimeTypes = struct { - TextPlain string - OctetStream string -}{"text/plain", "application/octet-stream"} - -// DetectContentType returns a best guess representation of the MIME -// content type for the bytes at c. The value detected by -// http.DetectContentType is guaranteed not be nil, defaulting to -// application/octet-stream when a better guess cannot be made. The -// result of this detection is then run through mime.ParseMediaType() -// which separates the actual MIME string from any parameters. -func DetectContentType(c []byte) (string, map[string]string, error) { - - ct := http.DetectContentType(c) - contentType, args, err := mime.ParseMediaType(ct) - if err != nil { - return "", nil, err - } - - return contentType, args, nil -} diff --git a/components/cli/vendor/github.com/docker/docker/pkg/idtools/idtools.go b/components/cli/vendor/github.com/docker/docker/pkg/idtools/idtools.go index 6bca466286..68a072db22 100644 --- a/components/cli/vendor/github.com/docker/docker/pkg/idtools/idtools.go +++ b/components/cli/vendor/github.com/docker/docker/pkg/idtools/idtools.go @@ -37,49 +37,56 @@ const ( // MkdirAllAs creates a directory (include any along the path) and then modifies // ownership to the requested uid/gid. If the directory already exists, this // function will still change ownership to the requested uid/gid pair. +// Deprecated: Use MkdirAllAndChown func MkdirAllAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { return mkdirAs(path, mode, ownerUID, ownerGID, true, true) } -// MkdirAllNewAs creates a directory (include any along the path) and then modifies -// ownership ONLY of newly created directories to the requested uid/gid. If the -// directories along the path exist, no change of ownership will be performed -func MkdirAllNewAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { - return mkdirAs(path, mode, ownerUID, ownerGID, true, false) -} - // MkdirAs creates a directory and then modifies ownership to the requested uid/gid. // If the directory already exists, this function still changes ownership +// Deprecated: Use MkdirAndChown with a IDPair func MkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { return mkdirAs(path, mode, ownerUID, ownerGID, false, true) } +// MkdirAllAndChown creates a directory (include any along the path) and then modifies +// ownership to the requested uid/gid. If the directory already exists, this +// function will still change ownership to the requested uid/gid pair. +func MkdirAllAndChown(path string, mode os.FileMode, ids IDPair) error { + return mkdirAs(path, mode, ids.UID, ids.GID, true, true) +} + +// MkdirAndChown creates a directory and then modifies ownership to the requested uid/gid. +// If the directory already exists, this function still changes ownership +func MkdirAndChown(path string, mode os.FileMode, ids IDPair) error { + return mkdirAs(path, mode, ids.UID, ids.GID, false, true) +} + +// MkdirAllAndChownNew creates a directory (include any along the path) and then modifies +// ownership ONLY of newly created directories to the requested uid/gid. If the +// directories along the path exist, no change of ownership will be performed +func MkdirAllAndChownNew(path string, mode os.FileMode, ids IDPair) error { + return mkdirAs(path, mode, ids.UID, ids.GID, true, false) +} + // GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps. // If the maps are empty, then the root uid/gid will default to "real" 0/0 func GetRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) { - var uid, gid int - - if uidMap != nil { - xUID, err := ToHost(0, uidMap) - if err != nil { - return -1, -1, err - } - uid = xUID + uid, err := toHost(0, uidMap) + if err != nil { + return -1, -1, err } - if gidMap != nil { - xGID, err := ToHost(0, gidMap) - if err != nil { - return -1, -1, err - } - gid = xGID + gid, err := toHost(0, gidMap) + if err != nil { + return -1, -1, err } return uid, gid, nil } -// ToContainer takes an id mapping, and uses it to translate a +// toContainer takes an id mapping, and uses it to translate a // host ID to the remapped ID. If no map is provided, then the translation // assumes a 1-to-1 mapping and returns the passed in id -func ToContainer(hostID int, idMap []IDMap) (int, error) { +func toContainer(hostID int, idMap []IDMap) (int, error) { if idMap == nil { return hostID, nil } @@ -92,10 +99,10 @@ func ToContainer(hostID int, idMap []IDMap) (int, error) { return -1, fmt.Errorf("Host ID %d cannot be mapped to a container ID", hostID) } -// ToHost takes an id mapping and a remapped ID, and translates the +// toHost takes an id mapping and a remapped ID, and translates the // ID to the mapped host ID. If no map is provided, then the translation // assumes a 1-to-1 mapping and returns the passed in id # -func ToHost(contID int, idMap []IDMap) (int, error) { +func toHost(contID int, idMap []IDMap) (int, error) { if idMap == nil { return contID, nil } @@ -108,26 +115,101 @@ func ToHost(contID int, idMap []IDMap) (int, error) { return -1, fmt.Errorf("Container ID %d cannot be mapped to a host ID", contID) } -// CreateIDMappings takes a requested user and group name and +// IDPair is a UID and GID pair +type IDPair struct { + UID int + GID int +} + +// IDMappings contains a mappings of UIDs and GIDs +type IDMappings struct { + uids []IDMap + gids []IDMap +} + +// NewIDMappings takes a requested user and group name and // using the data from /etc/sub{uid,gid} ranges, creates the // proper uid and gid remapping ranges for that user/group pair -func CreateIDMappings(username, groupname string) ([]IDMap, []IDMap, error) { +func NewIDMappings(username, groupname string) (*IDMappings, error) { subuidRanges, err := parseSubuid(username) if err != nil { - return nil, nil, err + return nil, err } subgidRanges, err := parseSubgid(groupname) if err != nil { - return nil, nil, err + return nil, err } if len(subuidRanges) == 0 { - return nil, nil, fmt.Errorf("No subuid ranges found for user %q", username) + return nil, fmt.Errorf("No subuid ranges found for user %q", username) } if len(subgidRanges) == 0 { - return nil, nil, fmt.Errorf("No subgid ranges found for group %q", groupname) + return nil, fmt.Errorf("No subgid ranges found for group %q", groupname) } - return createIDMap(subuidRanges), createIDMap(subgidRanges), nil + return &IDMappings{ + uids: createIDMap(subuidRanges), + gids: createIDMap(subgidRanges), + }, nil +} + +// NewIDMappingsFromMaps creates a new mapping from two slices +// Deprecated: this is a temporary shim while transitioning to IDMapping +func NewIDMappingsFromMaps(uids []IDMap, gids []IDMap) *IDMappings { + return &IDMappings{uids: uids, gids: gids} +} + +// RootPair returns a uid and gid pair for the root user. The error is ignored +// because a root user always exists, and the defaults are correct when the uid +// and gid maps are empty. +func (i *IDMappings) RootPair() IDPair { + uid, gid, _ := GetRootUIDGID(i.uids, i.gids) + return IDPair{UID: uid, GID: gid} +} + +// ToHost returns the host UID and GID for the container uid, gid. +// Remapping is only performed if the ids aren't already the remapped root ids +func (i *IDMappings) ToHost(pair IDPair) (IDPair, error) { + var err error + target := i.RootPair() + + if pair.UID != target.UID { + target.UID, err = toHost(pair.UID, i.uids) + if err != nil { + return target, err + } + } + + if pair.GID != target.GID { + target.GID, err = toHost(pair.GID, i.gids) + } + return target, err +} + +// ToContainer returns the container UID and GID for the host uid and gid +func (i *IDMappings) ToContainer(pair IDPair) (int, int, error) { + uid, err := toContainer(pair.UID, i.uids) + if err != nil { + return -1, -1, err + } + gid, err := toContainer(pair.GID, i.gids) + return uid, gid, err +} + +// Empty returns true if there are no id mappings +func (i *IDMappings) Empty() bool { + return len(i.uids) == 0 && len(i.gids) == 0 +} + +// UIDs return the UID mapping +// TODO: remove this once everything has been refactored to use pairs +func (i *IDMappings) UIDs() []IDMap { + return i.uids +} + +// GIDs return the UID mapping +// TODO: remove this once everything has been refactored to use pairs +func (i *IDMappings) GIDs() []IDMap { + return i.gids } func createIDMap(subidRanges ranges) []IDMap { diff --git a/components/cli/vendor/github.com/docker/docker/pkg/idtools/idtools_unix.go b/components/cli/vendor/github.com/docker/docker/pkg/idtools/idtools_unix.go index 7c7e82aee2..0b28249fa8 100644 --- a/components/cli/vendor/github.com/docker/docker/pkg/idtools/idtools_unix.go +++ b/components/cli/vendor/github.com/docker/docker/pkg/idtools/idtools_unix.go @@ -69,15 +69,15 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown // CanAccess takes a valid (existing) directory and a uid, gid pair and determines // if that uid, gid pair has access (execute bit) to the directory -func CanAccess(path string, uid, gid int) bool { +func CanAccess(path string, pair IDPair) bool { statInfo, err := system.Stat(path) if err != nil { return false } fileMode := os.FileMode(statInfo.Mode()) permBits := fileMode.Perm() - return accessible(statInfo.UID() == uint32(uid), - statInfo.GID() == uint32(gid), permBits) + return accessible(statInfo.UID() == uint32(pair.UID), + statInfo.GID() == uint32(pair.GID), permBits) } func accessible(isOwner, isGroup bool, perms os.FileMode) bool { diff --git a/components/cli/vendor/github.com/docker/docker/pkg/idtools/idtools_windows.go b/components/cli/vendor/github.com/docker/docker/pkg/idtools/idtools_windows.go index 49f67e78c1..8ed8353060 100644 --- a/components/cli/vendor/github.com/docker/docker/pkg/idtools/idtools_windows.go +++ b/components/cli/vendor/github.com/docker/docker/pkg/idtools/idtools_windows.go @@ -20,6 +20,6 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown // CanAccess takes a valid (existing) directory and a uid, gid pair and determines // if that uid, gid pair has access (execute bit) to the directory // Windows does not require/support this function, so always return true -func CanAccess(path string, uid, gid int) bool { +func CanAccess(path string, pair IDPair) bool { return true } diff --git a/components/cli/vendor/github.com/docker/docker/pkg/ioutils/multireader.go b/components/cli/vendor/github.com/docker/docker/pkg/ioutils/multireader.go deleted file mode 100644 index edb043ddc3..0000000000 --- a/components/cli/vendor/github.com/docker/docker/pkg/ioutils/multireader.go +++ /dev/null @@ -1,224 +0,0 @@ -package ioutils - -import ( - "bytes" - "fmt" - "io" - "os" -) - -type pos struct { - idx int - offset int64 -} - -type multiReadSeeker struct { - readers []io.ReadSeeker - pos *pos - posIdx map[io.ReadSeeker]int -} - -func (r *multiReadSeeker) Seek(offset int64, whence int) (int64, error) { - var tmpOffset int64 - switch whence { - case os.SEEK_SET: - for i, rdr := range r.readers { - // get size of the current reader - s, err := rdr.Seek(0, os.SEEK_END) - if err != nil { - return -1, err - } - - if offset > tmpOffset+s { - if i == len(r.readers)-1 { - rdrOffset := s + (offset - tmpOffset) - if _, err := rdr.Seek(rdrOffset, os.SEEK_SET); err != nil { - return -1, err - } - r.pos = &pos{i, rdrOffset} - return offset, nil - } - - tmpOffset += s - continue - } - - rdrOffset := offset - tmpOffset - idx := i - - rdr.Seek(rdrOffset, os.SEEK_SET) - // make sure all following readers are at 0 - for _, rdr := range r.readers[i+1:] { - rdr.Seek(0, os.SEEK_SET) - } - - if rdrOffset == s && i != len(r.readers)-1 { - idx++ - rdrOffset = 0 - } - r.pos = &pos{idx, rdrOffset} - return offset, nil - } - case os.SEEK_END: - for _, rdr := range r.readers { - s, err := rdr.Seek(0, os.SEEK_END) - if err != nil { - return -1, err - } - tmpOffset += s - } - r.Seek(tmpOffset+offset, os.SEEK_SET) - return tmpOffset + offset, nil - case os.SEEK_CUR: - if r.pos == nil { - return r.Seek(offset, os.SEEK_SET) - } - // Just return the current offset - if offset == 0 { - return r.getCurOffset() - } - - curOffset, err := r.getCurOffset() - if err != nil { - return -1, err - } - rdr, rdrOffset, err := r.getReaderForOffset(curOffset + offset) - if err != nil { - return -1, err - } - - r.pos = &pos{r.posIdx[rdr], rdrOffset} - return curOffset + offset, nil - default: - return -1, fmt.Errorf("Invalid whence: %d", whence) - } - - return -1, fmt.Errorf("Error seeking for whence: %d, offset: %d", whence, offset) -} - -func (r *multiReadSeeker) getReaderForOffset(offset int64) (io.ReadSeeker, int64, error) { - - var offsetTo int64 - - for _, rdr := range r.readers { - size, err := getReadSeekerSize(rdr) - if err != nil { - return nil, -1, err - } - if offsetTo+size > offset { - return rdr, offset - offsetTo, nil - } - if rdr == r.readers[len(r.readers)-1] { - return rdr, offsetTo + offset, nil - } - offsetTo += size - } - - return nil, 0, nil -} - -func (r *multiReadSeeker) getCurOffset() (int64, error) { - var totalSize int64 - for _, rdr := range r.readers[:r.pos.idx+1] { - if r.posIdx[rdr] == r.pos.idx { - totalSize += r.pos.offset - break - } - - size, err := getReadSeekerSize(rdr) - if err != nil { - return -1, fmt.Errorf("error getting seeker size: %v", err) - } - totalSize += size - } - return totalSize, nil -} - -func (r *multiReadSeeker) getOffsetToReader(rdr io.ReadSeeker) (int64, error) { - var offset int64 - for _, r := range r.readers { - if r == rdr { - break - } - - size, err := getReadSeekerSize(rdr) - if err != nil { - return -1, err - } - offset += size - } - return offset, nil -} - -func (r *multiReadSeeker) Read(b []byte) (int, error) { - if r.pos == nil { - // make sure all readers are at 0 - r.Seek(0, os.SEEK_SET) - } - - bLen := int64(len(b)) - buf := bytes.NewBuffer(nil) - var rdr io.ReadSeeker - - for _, rdr = range r.readers[r.pos.idx:] { - readBytes, err := io.CopyN(buf, rdr, bLen) - if err != nil && err != io.EOF { - return -1, err - } - bLen -= readBytes - - if bLen == 0 { - break - } - } - - rdrPos, err := rdr.Seek(0, os.SEEK_CUR) - if err != nil { - return -1, err - } - r.pos = &pos{r.posIdx[rdr], rdrPos} - return buf.Read(b) -} - -func getReadSeekerSize(rdr io.ReadSeeker) (int64, error) { - // save the current position - pos, err := rdr.Seek(0, os.SEEK_CUR) - if err != nil { - return -1, err - } - - // get the size - size, err := rdr.Seek(0, os.SEEK_END) - if err != nil { - return -1, err - } - - // reset the position - if _, err := rdr.Seek(pos, os.SEEK_SET); err != nil { - return -1, err - } - return size, nil -} - -// MultiReadSeeker returns a ReadSeeker that's the logical concatenation of the provided -// input readseekers. After calling this method the initial position is set to the -// beginning of the first ReadSeeker. At the end of a ReadSeeker, Read always advances -// to the beginning of the next ReadSeeker and returns EOF at the end of the last ReadSeeker. -// Seek can be used over the sum of lengths of all readseekers. -// -// When a MultiReadSeeker is used, no Read and Seek operations should be made on -// its ReadSeeker components. Also, users should make no assumption on the state -// of individual readseekers while the MultiReadSeeker is used. -func MultiReadSeeker(readers ...io.ReadSeeker) io.ReadSeeker { - if len(readers) == 1 { - return readers[0] - } - idx := make(map[io.ReadSeeker]int) - for i, rdr := range readers { - idx[rdr] = i - } - return &multiReadSeeker{ - readers: readers, - posIdx: idx, - } -} diff --git a/components/cli/vendor/github.com/docker/docker/pkg/pools/pools.go b/components/cli/vendor/github.com/docker/docker/pkg/pools/pools.go index 5c5aead698..6a111a3ba7 100644 --- a/components/cli/vendor/github.com/docker/docker/pkg/pools/pools.go +++ b/components/cli/vendor/github.com/docker/docker/pkg/pools/pools.go @@ -17,15 +17,16 @@ import ( "github.com/docker/docker/pkg/ioutils" ) +const buffer32K = 32 * 1024 + var ( // BufioReader32KPool is a pool which returns bufio.Reader with a 32K buffer. BufioReader32KPool = newBufioReaderPoolWithSize(buffer32K) // BufioWriter32KPool is a pool which returns bufio.Writer with a 32K buffer. BufioWriter32KPool = newBufioWriterPoolWithSize(buffer32K) + buffer32KPool = newBufferPoolWithSize(buffer32K) ) -const buffer32K = 32 * 1024 - // BufioReaderPool is a bufio reader that uses sync.Pool. type BufioReaderPool struct { pool sync.Pool @@ -54,11 +55,31 @@ func (bufPool *BufioReaderPool) Put(b *bufio.Reader) { bufPool.pool.Put(b) } +type bufferPool struct { + pool sync.Pool +} + +func newBufferPoolWithSize(size int) *bufferPool { + return &bufferPool{ + pool: sync.Pool{ + New: func() interface{} { return make([]byte, size) }, + }, + } +} + +func (bp *bufferPool) Get() []byte { + return bp.pool.Get().([]byte) +} + +func (bp *bufferPool) Put(b []byte) { + bp.pool.Put(b) +} + // Copy is a convenience wrapper which uses a buffer to avoid allocation in io.Copy. func Copy(dst io.Writer, src io.Reader) (written int64, err error) { - buf := BufioReader32KPool.Get(src) - written, err = io.Copy(dst, buf) - BufioReader32KPool.Put(buf) + buf := buffer32KPool.Get() + written, err = io.CopyBuffer(dst, src, buf) + buffer32KPool.Put(buf) return } diff --git a/components/cli/vendor/github.com/docker/docker/pkg/system/syscall_windows.go b/components/cli/vendor/github.com/docker/docker/pkg/system/syscall_windows.go index 1f311874f4..c328a6fb86 100644 --- a/components/cli/vendor/github.com/docker/docker/pkg/system/syscall_windows.go +++ b/components/cli/vendor/github.com/docker/docker/pkg/system/syscall_windows.go @@ -8,8 +8,9 @@ import ( ) var ( - ntuserApiset = syscall.NewLazyDLL("ext-ms-win-ntuser-window-l1-1-0") - procGetVersionExW = modkernel32.NewProc("GetVersionExW") + ntuserApiset = syscall.NewLazyDLL("ext-ms-win-ntuser-window-l1-1-0") + procGetVersionExW = modkernel32.NewProc("GetVersionExW") + procGetProductInfo = modkernel32.NewProc("GetProductInfo") ) // OSVersion is a wrapper for Windows version information @@ -66,6 +67,22 @@ func IsWindowsClient() bool { return osviex.ProductType == verNTWorkstation } +// IsIoTCore returns true if the currently running image is based off of +// Windows 10 IoT Core. +// @engine maintainers - this function should not be removed or modified as it +// is used to enforce licensing restrictions on Windows. +func IsIoTCore() bool { + var returnedProductType uint32 + r1, _, err := procGetProductInfo.Call(6, 1, 0, 0, uintptr(unsafe.Pointer(&returnedProductType))) + if r1 == 0 { + logrus.Warnf("GetProductInfo failed - assuming this is not IoT: %v", err) + return false + } + const productIoTUAP = 0x0000007B + const productIoTUAPCommercial = 0x00000083 + return returnedProductType == productIoTUAP || returnedProductType == productIoTUAPCommercial +} + // Unmount is a platform-specific helper function to call // the unmount syscall. Not supported on Windows func Unmount(dest string) error { diff --git a/components/cli/vendor/github.com/docker/docker/pkg/term/tc_solaris_cgo.go b/components/cli/vendor/github.com/docker/docker/pkg/term/tc_solaris_cgo.go index ebbded77c8..50234affc0 100644 --- a/components/cli/vendor/github.com/docker/docker/pkg/term/tc_solaris_cgo.go +++ b/components/cli/vendor/github.com/docker/docker/pkg/term/tc_solaris_cgo.go @@ -13,7 +13,7 @@ import ( import "C" // Termios is the Unix API for terminal I/O. -// It is passthrough for syscall.Termios in order to make it portable with +// It is passthrough for unix.Termios in order to make it portable with // other platforms where it is not available or handled differently. type Termios unix.Termios @@ -28,11 +28,11 @@ func MakeRaw(fd uintptr) (*State, error) { newState := oldState.termios - newState.Iflag &^= (syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON | syscall.IXANY) - newState.Oflag &^= syscall.OPOST - newState.Lflag &^= (syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN) - newState.Cflag &^= (syscall.CSIZE | syscall.PARENB) - newState.Cflag |= syscall.CS8 + newState.Iflag &^= (unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON | unix.IXANY) + newState.Oflag &^= unix.OPOST + newState.Lflag &^= (unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN) + newState.Cflag &^= (unix.CSIZE | unix.PARENB) + newState.Cflag |= unix.CS8 /* VMIN is the minimum number of characters that needs to be read in non-canonical mode for it to be returned diff --git a/components/cli/vendor/github.com/docker/docker/pkg/term/termios_linux.go b/components/cli/vendor/github.com/docker/docker/pkg/term/termios_linux.go index b1d9c8d1b5..31bfa8419e 100644 --- a/components/cli/vendor/github.com/docker/docker/pkg/term/termios_linux.go +++ b/components/cli/vendor/github.com/docker/docker/pkg/term/termios_linux.go @@ -26,7 +26,7 @@ func MakeRaw(fd uintptr) (*State, error) { newState := oldState.termios newState.Iflag &^= (unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON) - newState.Oflag |= unix.OPOST + newState.Oflag &^= unix.OPOST newState.Lflag &^= (unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN) newState.Cflag &^= (unix.CSIZE | unix.PARENB) newState.Cflag |= unix.CS8 diff --git a/components/cli/vendor/github.com/docker/docker/registry/config_unix.go b/components/cli/vendor/github.com/docker/docker/registry/config_unix.go index d692e8ef50..fdc39a1d68 100644 --- a/components/cli/vendor/github.com/docker/docker/registry/config_unix.go +++ b/components/cli/vendor/github.com/docker/docker/registry/config_unix.go @@ -21,5 +21,5 @@ func cleanPath(s string) string { // installCliPlatformFlags handles any platform specific flags for the service. func (options *ServiceOptions) installCliPlatformFlags(flags *pflag.FlagSet) { - flags.BoolVar(&options.V2Only, "disable-legacy-registry", false, "Disable contacting legacy registries") + flags.BoolVar(&options.V2Only, "disable-legacy-registry", true, "Disable contacting legacy registries") } diff --git a/components/cli/vendor/github.com/docker/docker/pkg/httputils/resumablerequestreader.go b/components/cli/vendor/github.com/docker/docker/registry/resumable/resumablerequestreader.go similarity index 66% rename from components/cli/vendor/github.com/docker/docker/pkg/httputils/resumablerequestreader.go rename to components/cli/vendor/github.com/docker/docker/registry/resumable/resumablerequestreader.go index de488fb531..5403c76847 100644 --- a/components/cli/vendor/github.com/docker/docker/pkg/httputils/resumablerequestreader.go +++ b/components/cli/vendor/github.com/docker/docker/registry/resumable/resumablerequestreader.go @@ -1,4 +1,4 @@ -package httputils +package resumable import ( "fmt" @@ -9,7 +9,7 @@ import ( "github.com/Sirupsen/logrus" ) -type resumableRequestReader struct { +type requestReader struct { client *http.Client request *http.Request lastRange int64 @@ -20,22 +20,22 @@ type resumableRequestReader struct { waitDuration time.Duration } -// ResumableRequestReader makes it possible to resume reading a request's body transparently +// NewRequestReader makes it possible to resume reading a request's body transparently // maxfail is the number of times we retry to make requests again (not resumes) // totalsize is the total length of the body; auto detect if not provided -func ResumableRequestReader(c *http.Client, r *http.Request, maxfail uint32, totalsize int64) io.ReadCloser { - return &resumableRequestReader{client: c, request: r, maxFailures: maxfail, totalSize: totalsize, waitDuration: 5 * time.Second} +func NewRequestReader(c *http.Client, r *http.Request, maxfail uint32, totalsize int64) io.ReadCloser { + return &requestReader{client: c, request: r, maxFailures: maxfail, totalSize: totalsize, waitDuration: 5 * time.Second} } -// ResumableRequestReaderWithInitialResponse makes it possible to resume +// NewRequestReaderWithInitialResponse makes it possible to resume // reading the body of an already initiated request. -func ResumableRequestReaderWithInitialResponse(c *http.Client, r *http.Request, maxfail uint32, totalsize int64, initialResponse *http.Response) io.ReadCloser { - return &resumableRequestReader{client: c, request: r, maxFailures: maxfail, totalSize: totalsize, currentResponse: initialResponse, waitDuration: 5 * time.Second} +func NewRequestReaderWithInitialResponse(c *http.Client, r *http.Request, maxfail uint32, totalsize int64, initialResponse *http.Response) io.ReadCloser { + return &requestReader{client: c, request: r, maxFailures: maxfail, totalSize: totalsize, currentResponse: initialResponse, waitDuration: 5 * time.Second} } -func (r *resumableRequestReader) Read(p []byte) (n int, err error) { +func (r *requestReader) Read(p []byte) (n int, err error) { if r.client == nil || r.request == nil { - return 0, fmt.Errorf("client and request can't be nil\n") + return 0, fmt.Errorf("client and request can't be nil") } isFreshRequest := false if r.lastRange != 0 && r.currentResponse == nil { @@ -81,14 +81,14 @@ func (r *resumableRequestReader) Read(p []byte) (n int, err error) { return n, err } -func (r *resumableRequestReader) Close() error { +func (r *requestReader) Close() error { r.cleanUpResponse() r.client = nil r.request = nil return nil } -func (r *resumableRequestReader) cleanUpResponse() { +func (r *requestReader) cleanUpResponse() { if r.currentResponse != nil { r.currentResponse.Body.Close() r.currentResponse = nil diff --git a/components/cli/vendor/github.com/docker/docker/registry/session.go b/components/cli/vendor/github.com/docker/docker/registry/session.go index c71e778037..9d7f32193f 100644 --- a/components/cli/vendor/github.com/docker/docker/registry/session.go +++ b/components/cli/vendor/github.com/docker/docker/registry/session.go @@ -23,10 +23,11 @@ import ( "github.com/docker/distribution/registry/api/errcode" "github.com/docker/docker/api/types" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/pkg/httputils" "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/tarsum" + "github.com/docker/docker/registry/resumable" ) var ( @@ -226,7 +227,7 @@ func (r *Session) GetRemoteHistory(imgID, registry string) ([]string, error) { if res.StatusCode == 401 { return nil, errcode.ErrorCodeUnauthorized.WithArgs() } - return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch remote history for %s", res.StatusCode, imgID), res) + return nil, newJSONError(fmt.Sprintf("Server error: %d trying to fetch remote history for %s", res.StatusCode, imgID), res) } var history []string @@ -246,7 +247,7 @@ func (r *Session) LookupRemoteImage(imgID, registry string) error { } res.Body.Close() if res.StatusCode != 200 { - return httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d", res.StatusCode), res) + return newJSONError(fmt.Sprintf("HTTP code %d", res.StatusCode), res) } return nil } @@ -259,7 +260,7 @@ func (r *Session) GetRemoteImageJSON(imgID, registry string) ([]byte, int64, err } defer res.Body.Close() if res.StatusCode != 200 { - return nil, -1, httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d", res.StatusCode), res) + return nil, -1, newJSONError(fmt.Sprintf("HTTP code %d", res.StatusCode), res) } // if the size header is not present, then set it to '-1' imageSize := int64(-1) @@ -313,7 +314,7 @@ func (r *Session) GetRemoteImageLayer(imgID, registry string, imgSize int64) (io if res.Header.Get("Accept-Ranges") == "bytes" && imgSize > 0 { logrus.Debug("server supports resume") - return httputils.ResumableRequestReaderWithInitialResponse(r.client, req, 5, imgSize, res), nil + return resumable.NewRequestReaderWithInitialResponse(r.client, req, 5, imgSize, res), nil } logrus.Debug("server doesn't support resume") return res.Body, nil @@ -444,13 +445,13 @@ func (r *Session) GetRepositoryData(name reference.Named) (*RepositoryData, erro // TODO: Right now we're ignoring checksums in the response body. // In the future, we need to use them to check image validity. if res.StatusCode == 404 { - return nil, httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code: %d", res.StatusCode), res) + return nil, newJSONError(fmt.Sprintf("HTTP code: %d", res.StatusCode), res) } else if res.StatusCode != 200 { errBody, err := ioutil.ReadAll(res.Body) if err != nil { logrus.Debugf("Error reading response body: %s", err) } - return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to pull repository %s: %q", res.StatusCode, reference.Path(name), errBody), res) + return nil, newJSONError(fmt.Sprintf("Error: Status %d trying to pull repository %s: %q", res.StatusCode, reference.Path(name), errBody), res) } var endpoints []string @@ -537,12 +538,12 @@ func (r *Session) PushImageJSONRegistry(imgData *ImgData, jsonRaw []byte, regist } defer res.Body.Close() if res.StatusCode == 401 && strings.HasPrefix(registry, "http://") { - return httputils.NewHTTPRequestError("HTTP code 401, Docker will not send auth headers over HTTP.", res) + return newJSONError("HTTP code 401, Docker will not send auth headers over HTTP.", res) } if res.StatusCode != 200 { errBody, err := ioutil.ReadAll(res.Body) if err != nil { - return httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res) + return newJSONError(fmt.Sprintf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res) } var jsonBody map[string]string if err := json.Unmarshal(errBody, &jsonBody); err != nil { @@ -550,7 +551,7 @@ func (r *Session) PushImageJSONRegistry(imgData *ImgData, jsonRaw []byte, regist } else if jsonBody["error"] == "Image already exists" { return ErrAlreadyExists } - return httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata: %q", res.StatusCode, errBody), res) + return newJSONError(fmt.Sprintf("HTTP code %d while uploading metadata: %q", res.StatusCode, errBody), res) } return nil } @@ -591,9 +592,9 @@ func (r *Session) PushImageLayerRegistry(imgID string, layer io.Reader, registry if res.StatusCode != 200 { errBody, err := ioutil.ReadAll(res.Body) if err != nil { - return "", "", httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res) + return "", "", newJSONError(fmt.Sprintf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res) } - return "", "", httputils.NewHTTPRequestError(fmt.Sprintf("Received HTTP code %d while uploading layer: %q", res.StatusCode, errBody), res) + return "", "", newJSONError(fmt.Sprintf("Received HTTP code %d while uploading layer: %q", res.StatusCode, errBody), res) } checksumPayload = "sha256:" + hex.EncodeToString(h.Sum(nil)) @@ -619,7 +620,7 @@ func (r *Session) PushRegistryTag(remote reference.Named, revision, tag, registr } res.Body.Close() if res.StatusCode != 200 && res.StatusCode != 201 { - return httputils.NewHTTPRequestError(fmt.Sprintf("Internal server error: %d trying to push tag %s on %s", res.StatusCode, tag, reference.Path(remote)), res) + return newJSONError(fmt.Sprintf("Internal server error: %d trying to push tag %s on %s", res.StatusCode, tag, reference.Path(remote)), res) } return nil } @@ -683,7 +684,7 @@ func (r *Session) PushImageJSONIndex(remote reference.Named, imgList []*ImgData, if err != nil { logrus.Debugf("Error reading response body: %s", err) } - return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push repository %s: %q", res.StatusCode, reference.Path(remote), errBody), res) + return nil, newJSONError(fmt.Sprintf("Error: Status %d trying to push repository %s: %q", res.StatusCode, reference.Path(remote), errBody), res) } tokens = res.Header["X-Docker-Token"] logrus.Debugf("Auth token: %v", tokens) @@ -701,7 +702,7 @@ func (r *Session) PushImageJSONIndex(remote reference.Named, imgList []*ImgData, if err != nil { logrus.Debugf("Error reading response body: %s", err) } - return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push checksums %s: %q", res.StatusCode, reference.Path(remote), errBody), res) + return nil, newJSONError(fmt.Sprintf("Error: Status %d trying to push checksums %s: %q", res.StatusCode, reference.Path(remote), errBody), res) } } @@ -750,25 +751,12 @@ func (r *Session) SearchRepositories(term string, limit int) (*registrytypes.Sea } defer res.Body.Close() if res.StatusCode != 200 { - return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Unexpected status code %d", res.StatusCode), res) + return nil, newJSONError(fmt.Sprintf("Unexpected status code %d", res.StatusCode), res) } result := new(registrytypes.SearchResults) return result, json.NewDecoder(res.Body).Decode(result) } -// GetAuthConfig returns the authentication settings for a session -// TODO(tiborvass): remove this once registry client v2 is vendored -func (r *Session) GetAuthConfig(withPasswd bool) *types.AuthConfig { - password := "" - if withPasswd { - password = r.authConfig.Password - } - return &types.AuthConfig{ - Username: r.authConfig.Username, - Password: password, - } -} - func isTimeout(err error) bool { type timeout interface { Timeout() bool @@ -781,3 +769,10 @@ func isTimeout(err error) bool { t, ok := e.(timeout) return ok && t.Timeout() } + +func newJSONError(msg string, res *http.Response) error { + return &jsonmessage.JSONError{ + Message: msg, + Code: res.StatusCode, + } +} diff --git a/components/cli/vendor/github.com/docker/docker/runconfig/hostconfig.go b/components/cli/vendor/github.com/docker/docker/runconfig/hostconfig.go index e8eede1505..24aed1935e 100644 --- a/components/cli/vendor/github.com/docker/docker/runconfig/hostconfig.go +++ b/components/cli/vendor/github.com/docker/docker/runconfig/hostconfig.go @@ -45,7 +45,7 @@ func validateNetContainerMode(c *container.Config, hc *container.HostConfig) err parts := strings.Split(string(hc.NetworkMode), ":") if parts[0] == "container" { if len(parts) < 2 || parts[1] == "" { - return fmt.Errorf("--net: invalid net mode: invalid container format container:") + return fmt.Errorf("Invalid network mode: invalid container format container:") } } diff --git a/components/cli/vendor/github.com/docker/docker/runconfig/hostconfig_unix.go b/components/cli/vendor/github.com/docker/docker/runconfig/hostconfig_unix.go index a60daa8787..55df5da3ff 100644 --- a/components/cli/vendor/github.com/docker/docker/runconfig/hostconfig_unix.go +++ b/components/cli/vendor/github.com/docker/docker/runconfig/hostconfig_unix.go @@ -55,7 +55,7 @@ func validateIsolation(hc *container.HostConfig) error { return nil } if !hc.Isolation.IsValid() { - return fmt.Errorf("invalid --isolation: %q - %s only supports 'default'", hc.Isolation, runtime.GOOS) + return fmt.Errorf("Invalid isolation: %q - %s only supports 'default'", hc.Isolation, runtime.GOOS) } return nil } @@ -68,11 +68,11 @@ func validateQoS(hc *container.HostConfig) error { } if hc.IOMaximumBandwidth != 0 { - return fmt.Errorf("invalid QoS settings: %s does not support --io-maxbandwidth", runtime.GOOS) + return fmt.Errorf("Invalid QoS settings: %s does not support configuration of maximum bandwidth", runtime.GOOS) } if hc.IOMaximumIOps != 0 { - return fmt.Errorf("invalid QoS settings: %s does not support --io-maxiops", runtime.GOOS) + return fmt.Errorf("Invalid QoS settings: %s does not support configuration of maximum IOPs", runtime.GOOS) } return nil } @@ -86,15 +86,15 @@ func validateResources(hc *container.HostConfig, si *sysinfo.SysInfo) error { } if hc.Resources.CPURealtimePeriod > 0 && !si.CPURealtimePeriod { - return fmt.Errorf("invalid --cpu-rt-period: Your kernel does not support cgroup rt period") + return fmt.Errorf("Your kernel does not support cgroup cpu real-time period") } if hc.Resources.CPURealtimeRuntime > 0 && !si.CPURealtimeRuntime { - return fmt.Errorf("invalid --cpu-rt-runtime: Your kernel does not support cgroup rt runtime") + return fmt.Errorf("Your kernel does not support cgroup cpu real-time runtime") } if hc.Resources.CPURealtimePeriod != 0 && hc.Resources.CPURealtimeRuntime != 0 && hc.Resources.CPURealtimeRuntime > hc.Resources.CPURealtimePeriod { - return fmt.Errorf("invalid --cpu-rt-runtime: rt runtime cannot be higher than rt period") + return fmt.Errorf("cpu real-time runtime cannot be higher than cpu real-time period") } return nil } diff --git a/components/cli/vendor/github.com/docker/docker/runconfig/hostconfig_windows.go b/components/cli/vendor/github.com/docker/docker/runconfig/hostconfig_windows.go index 9ca93ae508..5eb956d1b4 100644 --- a/components/cli/vendor/github.com/docker/docker/runconfig/hostconfig_windows.go +++ b/components/cli/vendor/github.com/docker/docker/runconfig/hostconfig_windows.go @@ -31,7 +31,7 @@ func validateNetMode(c *container.Config, hc *container.HostConfig) error { } if hc.NetworkMode.IsContainer() && hc.Isolation.IsHyperV() { - return fmt.Errorf("net mode --net=container: unsupported for hyperv isolation") + return fmt.Errorf("Using the network stack of another container is not supported while using Hyper-V Containers") } return nil @@ -46,7 +46,7 @@ func validateIsolation(hc *container.HostConfig) error { return nil } if !hc.Isolation.IsValid() { - return fmt.Errorf("invalid --isolation: %q. Windows supports 'default', 'process', or 'hyperv'", hc.Isolation) + return fmt.Errorf("Invalid isolation: %q. Windows supports 'default', 'process', or 'hyperv'", hc.Isolation) } return nil } @@ -63,10 +63,10 @@ func validateResources(hc *container.HostConfig, si *sysinfo.SysInfo) error { return nil } if hc.Resources.CPURealtimePeriod != 0 { - return fmt.Errorf("invalid --cpu-rt-period: Windows does not support this feature") + return fmt.Errorf("Windows does not support CPU real-time period") } if hc.Resources.CPURealtimeRuntime != 0 { - return fmt.Errorf("invalid --cpu-rt-runtime: Windows does not support this feature") + return fmt.Errorf("Windows does not support CPU real-time runtime") } return nil } @@ -78,7 +78,7 @@ func validatePrivileged(hc *container.HostConfig) error { return nil } if hc.Privileged { - return fmt.Errorf("invalid --privileged: Windows does not support this feature") + return fmt.Errorf("Windows does not support privileged mode") } return nil } @@ -90,7 +90,7 @@ func validateReadonlyRootfs(hc *container.HostConfig) error { return nil } if hc.ReadonlyRootfs { - return fmt.Errorf("invalid --read-only: Windows does not support this feature") + return fmt.Errorf("Windows does not support root filesystem in read-only mode") } return nil } diff --git a/components/cli/vendor/github.com/docker/docker/vendor.conf b/components/cli/vendor/github.com/docker/docker/vendor.conf index df4aa7e1e8..e524c518ab 100644 --- a/components/cli/vendor/github.com/docker/docker/vendor.conf +++ b/components/cli/vendor/github.com/docker/docker/vendor.conf @@ -1,7 +1,7 @@ # the following lines are in sorted order, FYI github.com/Azure/go-ansiterm 388960b655244e76e24c75f48631564eaefade62 github.com/Microsoft/hcsshim v0.5.17 -github.com/Microsoft/go-winio v0.4.0 +github.com/Microsoft/go-winio v0.4.2 github.com/Sirupsen/logrus v0.11.0 github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76 github.com/docker/libtrust 9cbd2a1374f46905c68a4eb3694a130610adc62a @@ -13,7 +13,7 @@ github.com/mattn/go-shellwords v1.0.3 github.com/tchap/go-patricia v2.2.6 github.com/vdemeester/shakers 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3 # forked golang.org/x/net package includes a patch for lazy loading trace templates -golang.org/x/net c427ad74c6d7a814201695e9ffde0c5d400a7674 +golang.org/x/net 7dcfb8076726a3fdd9353b6b8a1f1b6be6811bd6 golang.org/x/sys 8f0908ab3b2457e2e15403d3697c9ef5cb4b57a9 github.com/docker/go-units 9e638d38cf6977a37a8ea0078f3ee75a7cdb2dd1 github.com/docker/go-connections e15c02316c12de00874640cd76311849de2aeed5 @@ -26,7 +26,7 @@ github.com/imdario/mergo 0.2.1 golang.org/x/sync de49d9dcd27d4f764488181bea099dfe6179bcf0 #get libnetwork packages -github.com/docker/libnetwork b2bc1a68486ccf8ada503162d9f0df7d31bdd8fb +github.com/docker/libnetwork f4a15a0890383619ad797b3bd2481cc6f46a978d github.com/docker/go-events 18b43f1bc85d9cdd42c05a6cd2d444c7a200a894 github.com/armon/go-radix e39d623f12e8e41c7b5529e9a9dd67a1e2261f80 github.com/armon/go-metrics eb0af217e5e9747e41dd5303755356b62d28e3ec @@ -38,7 +38,7 @@ github.com/hashicorp/go-multierror fcdddc395df1ddf4247c69bd436e84cfa0733f7e github.com/hashicorp/serf 598c54895cc5a7b1a24a398d635e8c0ea0959870 github.com/docker/libkv 1d8431073ae03cdaedb198a89722f3aab6d418ef github.com/vishvananda/netns 604eaf189ee867d8c147fafc28def2394e878d25 -github.com/vishvananda/netlink 1e86b2bee5b6a7d377e4c02bb7f98209d6a7297c +github.com/vishvananda/netlink bd6d5de5ccef2d66b0a26177928d0d8895d7f969 github.com/BurntSushi/toml f706d00e3de6abe700c994cdd545a1a4915af060 github.com/samuel/go-zookeeper d0e0d8e11f318e000a8cc434616d69e329edc374 github.com/deckarep/golang-set ef32fa3046d9f249d399f98ebaf9be944430fd1d @@ -57,13 +57,13 @@ github.com/opencontainers/go-digest a6d0ee40d4207ea02364bd3b9e8e77b9159ba1eb github.com/mistifyio/go-zfs 22c9b32c84eb0d0c6f4043b6e90fc94073de92fa github.com/pborman/uuid v1.0 -google.golang.org/grpc v1.0.4 +google.golang.org/grpc v1.3.0 github.com/miekg/pkcs11 df8ae6ca730422dba20c768ff38ef7d79077a59f # When updating, also update RUNC_COMMIT in hack/dockerfile/binaries-commits accordingly -github.com/opencontainers/runc 992a5be178a62e026f4069f443c6164912adbf09 -github.com/opencontainers/runtime-spec v1.0.0-rc5 # specs +github.com/opencontainers/runc 2d41c047c83e09a6d61d464906feb2a2f3c52aa4 https://github.com/docker/runc github.com/opencontainers/image-spec f03dbe35d449c54915d235f1a3cf8f585a24babe +github.com/opencontainers/runtime-spec d42f1eb741e6361e858d83fc75aa6893b66292c4 # specs github.com/seccomp/libseccomp-golang 32f571b70023028bd57d9288c20efbcb237f3ce0 @@ -71,7 +71,7 @@ github.com/seccomp/libseccomp-golang 32f571b70023028bd57d9288c20efbcb237f3ce0 github.com/coreos/go-systemd v4 github.com/godbus/dbus v4.0.0 github.com/syndtr/gocapability 2c00daeb6c3b45114c80ac44119e7b8801fdd852 -github.com/golang/protobuf 8ee79997227bf9b34611aee7946ae64735e6fd93 +github.com/golang/protobuf 7a211bcf3bce0e3f1d74f9894916e6f116ae83b4 # gelf logging driver deps github.com/Graylog2/go-gelf 7029da823dad4ef3a876df61065156acb703b2ea @@ -97,17 +97,14 @@ golang.org/x/oauth2 96382aa079b72d8c014eb0c50f6c223d1e6a2de0 google.golang.org/api 3cc2e591b550923a2c5f0ab5a803feda924d5823 cloud.google.com/go 9d965e63e8cceb1b5d7977a202f0fcb8866d6525 github.com/googleapis/gax-go da06d194a00e19ce00d9011a13931c3f6f6887c7 -google.golang.org/genproto b3e7c2fb04031add52c4817f53f43757ccbf9c18 - -# native credentials -github.com/docker/docker-credential-helpers v0.5.0 +google.golang.org/genproto d80a6e20e776b0b17a324d0ba1ab50a39c8e8944 # containerd github.com/containerd/containerd 3addd840653146c90a254301d6c3a663c7fd6429 github.com/tonistiigi/fifo 1405643975692217d6720f8b54aeee1bf2cd5cf4 # cluster -github.com/docker/swarmkit 1a3e510517be82d18ac04380b5f71eddf06c2fc0 +github.com/docker/swarmkit a4bf0135f63fb60f0e76ae81579cde87f580db6e github.com/gogo/protobuf v0.4 github.com/cloudflare/cfssl 7fb22c8cba7ecaf98e4082d22d65800cf45e042a github.com/google/certificate-transparency d90e65c3a07988180c5b1ece71791c0b6506826e diff --git a/components/cli/vendor/github.com/docker/docker/volume/volume.go b/components/cli/vendor/github.com/docker/docker/volume/volume.go index 5135605281..2abfcc7b58 100644 --- a/components/cli/vendor/github.com/docker/docker/volume/volume.go +++ b/components/cli/vendor/github.com/docker/docker/volume/volume.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" "syscall" + "time" mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/pkg/idtools" @@ -64,6 +65,8 @@ type Volume interface { Mount(id string) (string, error) // Unmount unmounts the volume when it is no longer in use. Unmount(id string) error + // CreatedAt returns Volume Creation time + CreatedAt() (time.Time, error) // Status returns low-level status information about a volume Status() map[string]interface{} } @@ -146,7 +149,9 @@ func (m *MountPoint) Cleanup() error { // Setup sets up a mount point by either mounting the volume if it is // configured, or creating the source directory if supplied. -func (m *MountPoint) Setup(mountLabel string, rootUID, rootGID int) (path string, err error) { +// The, optional, checkFun parameter allows doing additional checking +// before creating the source directory on the host. +func (m *MountPoint) Setup(mountLabel string, rootIDs idtools.IDPair, checkFun func(m *MountPoint) error) (path string, err error) { defer func() { if err == nil { if label.RelabelNeeded(m.Mode) { @@ -181,9 +186,17 @@ func (m *MountPoint) Setup(mountLabel string, rootUID, rootGID int) (path string // system.MkdirAll() produces an error if m.Source exists and is a file (not a directory), if m.Type == mounttypes.TypeBind { + // Before creating the source directory on the host, invoke checkFun if it's not nil. One of + // the use case is to forbid creating the daemon socket as a directory if the daemon is in + // the process of shutting down. + if checkFun != nil { + if err := checkFun(m); err != nil { + return "", err + } + } // idtools.MkdirAllNewAs() produces an error if m.Source exists and is a file (not a directory) // also, makes sure that if the directory is created, the correct remapped rootUID/rootGID will own it - if err := idtools.MkdirAllNewAs(m.Source, 0755, rootUID, rootGID); err != nil { + if err := idtools.MkdirAllAndChownNew(m.Source, 0755, rootIDs); err != nil { if perr, ok := err.(*os.PathError); ok { if perr.Err != syscall.ENOTDIR { return "", errors.Wrapf(err, "error while creating mount source path '%s'", m.Source) diff --git a/components/cli/vendor/github.com/docker/libnetwork/LICENSE b/components/cli/vendor/github.com/docker/libnetwork/LICENSE deleted file mode 100644 index e06d208186..0000000000 --- a/components/cli/vendor/github.com/docker/libnetwork/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/components/cli/vendor/github.com/docker/libnetwork/README.md b/components/cli/vendor/github.com/docker/libnetwork/README.md deleted file mode 100644 index 536f8aa2b3..0000000000 --- a/components/cli/vendor/github.com/docker/libnetwork/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# libnetwork - networking for containers - -[![Circle CI](https://circleci.com/gh/docker/libnetwork/tree/master.svg?style=svg)](https://circleci.com/gh/docker/libnetwork/tree/master) [![Coverage Status](https://coveralls.io/repos/docker/libnetwork/badge.svg)](https://coveralls.io/r/docker/libnetwork) [![GoDoc](https://godoc.org/github.com/docker/libnetwork?status.svg)](https://godoc.org/github.com/docker/libnetwork) - -Libnetwork provides a native Go implementation for connecting containers - -The goal of libnetwork is to deliver a robust Container Network Model that provides a consistent programming interface and the required network abstractions for applications. - -#### Design -Please refer to the [design](docs/design.md) for more information. - -#### Using libnetwork - -There are many networking solutions available to suit a broad range of use-cases. libnetwork uses a driver / plugin model to support all of these solutions while abstracting the complexity of the driver implementations by exposing a simple and consistent Network Model to users. - - -```go -func main() { - if reexec.Init() { - return - } - - // Select and configure the network driver - networkType := "bridge" - - // Create a new controller instance - driverOptions := options.Generic{} - genericOption := make(map[string]interface{}) - genericOption[netlabel.GenericData] = driverOptions - controller, err := libnetwork.New(config.OptionDriverConfig(networkType, genericOption)) - if err != nil { - log.Fatalf("libnetwork.New: %s", err) - } - - // Create a network for containers to join. - // NewNetwork accepts Variadic optional arguments that libnetwork and Drivers can use. - network, err := controller.NewNetwork(networkType, "network1", "") - if err != nil { - log.Fatalf("controller.NewNetwork: %s", err) - } - - // For each new container: allocate IP and interfaces. The returned network - // settings will be used for container infos (inspect and such), as well as - // iptables rules for port publishing. This info is contained or accessible - // from the returned endpoint. - ep, err := network.CreateEndpoint("Endpoint1") - if err != nil { - log.Fatalf("network.CreateEndpoint: %s", err) - } - - // Create the sandbox for the container. - // NewSandbox accepts Variadic optional arguments which libnetwork can use. - sbx, err := controller.NewSandbox("container1", - libnetwork.OptionHostname("test"), - libnetwork.OptionDomainname("docker.io")) - if err != nil { - log.Fatalf("controller.NewSandbox: %s", err) - } - - // A sandbox can join the endpoint via the join api. - err = ep.Join(sbx) - if err != nil { - log.Fatalf("ep.Join: %s", err) - } - - // libnetwork client can check the endpoint's operational data via the Info() API - epInfo, err := ep.DriverInfo() - if err != nil { - log.Fatalf("ep.DriverInfo: %s", err) - } - - macAddress, ok := epInfo[netlabel.MacAddress] - if !ok { - log.Fatalf("failed to get mac address from endpoint info") - } - - fmt.Printf("Joined endpoint %s (%s) to sandbox %s (%s)\n", ep.Name(), macAddress, sbx.ContainerID(), sbx.Key()) -} -``` - -## Future -Please refer to [roadmap](ROADMAP.md) for more information. - -## Contributing - -Want to hack on libnetwork? [Docker's contributions guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md) apply. - -## Copyright and license -Code and documentation copyright 2015 Docker, inc. Code released under the Apache 2.0 license. Docs released under Creative commons. diff --git a/components/cli/vendor/github.com/docker/libnetwork/resolvconf/README.md b/components/cli/vendor/github.com/docker/libnetwork/resolvconf/README.md deleted file mode 100644 index cdda554ba5..0000000000 --- a/components/cli/vendor/github.com/docker/libnetwork/resolvconf/README.md +++ /dev/null @@ -1 +0,0 @@ -Package resolvconf provides utility code to query and update DNS configuration in /etc/resolv.conf diff --git a/components/cli/vendor/github.com/docker/libnetwork/resolvconf/dns/resolvconf.go b/components/cli/vendor/github.com/docker/libnetwork/resolvconf/dns/resolvconf.go deleted file mode 100644 index e348bc57f5..0000000000 --- a/components/cli/vendor/github.com/docker/libnetwork/resolvconf/dns/resolvconf.go +++ /dev/null @@ -1,26 +0,0 @@ -package dns - -import ( - "regexp" -) - -// IPLocalhost is a regex pattern for IPv4 or IPv6 loopback range. -const IPLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)` - -// IPv4Localhost is a regex pattern for IPv4 localhost address range. -const IPv4Localhost = `(127\.([0-9]{1,3}\.){2}[0-9]{1,3})` - -var localhostIPRegexp = regexp.MustCompile(IPLocalhost) -var localhostIPv4Regexp = regexp.MustCompile(IPv4Localhost) - -// 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) -} - -// IsIPv4Localhost returns true if ip matches the IPv4 localhost regular expression. -func IsIPv4Localhost(ip string) bool { - return localhostIPv4Regexp.MatchString(ip) -} diff --git a/components/cli/vendor/github.com/docker/libnetwork/vendor.conf b/components/cli/vendor/github.com/docker/libnetwork/vendor.conf deleted file mode 100644 index 45f2784192..0000000000 --- a/components/cli/vendor/github.com/docker/libnetwork/vendor.conf +++ /dev/null @@ -1,42 +0,0 @@ -github.com/Azure/go-ansiterm 04b7f292a41fcb5da32dda536c0807fc13e8351c -github.com/BurntSushi/toml f706d00e3de6abe700c994cdd545a1a4915af060 -github.com/Microsoft/go-winio ce2922f643c8fd76b46cadc7f404a06282678b34 -github.com/Microsoft/hcsshim e439b7d2b63f036d3a50c93a9e0b154a0d50e788 -github.com/Sirupsen/logrus 4b6ea7319e214d98c938f12692336f7ca9348d6b -github.com/armon/go-metrics eb0af217e5e9747e41dd5303755356b62d28e3ec -github.com/armon/go-radix e39d623f12e8e41c7b5529e9a9dd67a1e2261f80 -github.com/boltdb/bolt c6ba97b89e0454fec9aa92e1d33a4e2c5fc1f631 -github.com/codegangsta/cli a65b733b303f0055f8d324d805f393cd3e7a7904 -github.com/coreos/etcd 925d1d74cec8c3b169c52fd4b2dc234a35934fce -github.com/coreos/go-systemd b4a58d95188dd092ae20072bac14cece0e67c388 -github.com/deckarep/golang-set ef32fa3046d9f249d399f98ebaf9be944430fd1d - -github.com/docker/docker 9c96768eae4b3a65147b47a55c850c103ab8972d -github.com/docker/go-connections 34b5052da6b11e27f5f2e357b38b571ddddd3928 -github.com/docker/go-events 2e7d352816128aa84f4d29b2a21d400133701a0d -github.com/docker/go-units 8e2d4523730c73120e10d4652f36ad6010998f4e -github.com/docker/libkv 1d8431073ae03cdaedb198a89722f3aab6d418ef - -github.com/godbus/dbus 5f6efc7ef2759c81b7ba876593971bfce311eab3 -github.com/gogo/protobuf 8d70fb3182befc465c4a1eac8ad4d38ff49778e2 -github.com/golang/protobuf/proto f7137ae6b19afbfd61a94b746fda3b3fe0491874 -github.com/gorilla/context 215affda49addc4c8ef7e2534915df2c8c35c6cd -github.com/gorilla/mux 8096f47503459bcc74d1f4c487b7e6e42e5746b5 -github.com/hashicorp/consul/api 954aec66231b79c161a4122b023fbcad13047f79 -github.com/hashicorp/go-msgpack/codec 71c2886f5a673a35f909803f38ece5810165097b -github.com/hashicorp/go-multierror 2167c8ec40776024589f483a6b836489e47e1049 -github.com/hashicorp/memberlist 88ac4de0d1a0ca6def284b571342db3b777a4c37 -github.com/hashicorp/serf 598c54895cc5a7b1a24a398d635e8c0ea0959870 -github.com/mattn/go-shellwords 525bedee691b5a8df547cb5cf9f86b7fb1883e24 -github.com/miekg/dns d27455715200c7d3e321a1e5cadb27c9ee0b0f02 -github.com/opencontainers/runc/libcontainer ba1568de399395774ad84c2ace65937814c542ed -github.com/samuel/go-zookeeper/zk d0e0d8e11f318e000a8cc434616d69e329edc374 -github.com/seccomp/libseccomp-golang 1b506fc7c24eec5a3693cdcbed40d9c226cfc6a1 -github.com/stretchr/testify dab07ac62d4905d3e48d17dc549c684ac3b7c15a -github.com/syndtr/gocapability/capability 2c00daeb6c3b45114c80ac44119e7b8801fdd852 -github.com/ugorji/go f1f1a805ed361a0e078bb537e4ea78cd37dcf065 -github.com/vishvananda/netlink 1e86b2bee5b6a7d377e4c02bb7f98209d6a7297c -github.com/vishvananda/netns 604eaf189ee867d8c147fafc28def2394e878d25 -golang.org/x/net c427ad74c6d7a814201695e9ffde0c5d400a7674 -golang.org/x/sys 8f0908ab3b2457e2e15403d3697c9ef5cb4b57a9 -github.com/pkg/errors 839d9e913e063e28dfd0e6c7b7512793e0a48be9