Compare commits

..

1 Commits

Author SHA1 Message Date
Nate 95500effc3
Merge a418c68e38 into 185622986e 2024-10-08 12:17:46 -04:00
849 changed files with 14291 additions and 42567 deletions

View File

@ -62,7 +62,7 @@ jobs:
name: Update Go name: Update Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: 1.23.2 go-version: 1.22.8
- -
name: Initialize CodeQL name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v3

View File

@ -68,7 +68,7 @@ jobs:
name: Set up Go name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: 1.23.2 go-version: 1.22.8
- -
name: Test name: Test
run: | run: |

View File

@ -1,12 +1,12 @@
linters: linters:
enable: enable:
- bodyclose - bodyclose
- copyloopvar # Detects places where loop variables are copied.
- depguard - depguard
- dogsled - dogsled
- dupword # Detects duplicate words. - dupword # Detects duplicate words.
- durationcheck - durationcheck
- errchkjson - errchkjson
- exportloopref # Detects pointers to enclosing loop variables.
- gocritic # Metalinter; detects bugs, performance, and styling issues. - gocritic # Metalinter; detects bugs, performance, and styling issues.
- gocyclo - gocyclo
- gofumpt # Detects whether code was gofumpt-ed. - gofumpt # Detects whether code was gofumpt-ed.
@ -41,9 +41,6 @@ linters:
- errcheck - errcheck
run: run:
# prevent golangci-lint from deducting the go version to lint for through go.mod,
# which causes it to fallback to go1.17 semantics.
go: "1.23.2"
timeout: 5m timeout: 5m
linters-settings: linters-settings:
@ -55,13 +52,6 @@ linters-settings:
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
gocyclo: gocyclo:
min-complexity: 16 min-complexity: 16
gosec:
excludes:
- G104 # G104: Errors unhandled; (TODO: reduce unhandled errors, or explicitly ignore)
- G113 # G113: Potential uncontrolled memory consumption in Rat.SetString (CVE-2022-23772); (only affects go < 1.16.14. and go < 1.17.7)
- G115 # G115: integer overflow conversion; (TODO: verify these: https://github.com/docker/cli/issues/5584)
- G306 # G306: Expect WriteFile permissions to be 0600 or less (too restrictive; also flags "0o644" permissions)
- G307 # G307: Deferring unsafe method "*os.File" on type "Close" (also EXC0008); (TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close")
govet: govet:
enable: enable:
- shadow - shadow
@ -97,10 +87,6 @@ issues:
# The default exclusion rules are a bit too permissive, so copying the relevant ones below # The default exclusion rules are a bit too permissive, so copying the relevant ones below
exclude-use-default: false exclude-use-default: false
# This option has been defined when Go modules was not existed and when the
# golangci-lint core was different, this is not something we still recommend.
exclude-dirs-use-default: false
exclude: exclude:
- parameter .* always receives - parameter .* always receives
@ -118,9 +104,6 @@ issues:
# #
# These exclusion patterns are copied from the default excluses at: # These exclusion patterns are copied from the default excluses at:
# https://github.com/golangci/golangci-lint/blob/v1.44.0/pkg/config/issues.go#L10-L104 # https://github.com/golangci/golangci-lint/blob/v1.44.0/pkg/config/issues.go#L10-L104
#
# The default list of exclusions can be found at:
# https://golangci-lint.run/usage/false-positives/#default-exclusions
# EXC0001 # EXC0001
- text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv). is not checked" - text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv). is not checked"
@ -138,6 +121,11 @@ issues:
- text: "Subprocess launch(ed with variable|ing should be audited)" - text: "Subprocess launch(ed with variable|ing should be audited)"
linters: linters:
- gosec - gosec
# EXC0008
# TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close" (gosec)
- text: "G307"
linters:
- gosec
# EXC0009 # EXC0009
- text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)" - text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
linters: linters:
@ -147,6 +135,26 @@ issues:
linters: linters:
- gosec - gosec
# G113 Potential uncontrolled memory consumption in Rat.SetString (CVE-2022-23772)
# only affects gp < 1.16.14. and go < 1.17.7
- text: "G113"
linters:
- gosec
# TODO: G104: Errors unhandled. (gosec)
- text: "G104"
linters:
- gosec
# Looks like the match in "EXC0007" above doesn't catch this one
# TODO: consider upstreaming this to golangci-lint's default exclusion rules
- text: "G204: Subprocess launched with a potential tainted input or cmd arguments"
linters:
- gosec
# Looks like the match in "EXC0009" above doesn't catch this one
# TODO: consider upstreaming this to golangci-lint's default exclusion rules
- text: "G306: Expect WriteFile permissions to be 0600 or less"
linters:
- gosec
# TODO: make sure all packages have a description. Currently, there's 67 packages without. # TODO: make sure all packages have a description. Currently, there's 67 packages without.
- text: "package-comments: should have a package comment" - text: "package-comments: should have a package comment"
linters: linters:

View File

@ -66,7 +66,7 @@ anybody starts working on it.
We are always thrilled to receive pull requests. We do our best to process them We are always thrilled to receive pull requests. We do our best to process them
quickly. If your pull request is not accepted on the first try, quickly. If your pull request is not accepted on the first try,
don't get discouraged! Our contributor's guide explains [the review process we don't get discouraged! Our contributor's guide explains [the review process we
use for simple changes](https://github.com/docker/docker/blob/master/project/REVIEWING.md). use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contribution/).
### Talking to other Docker users and contributors ### Talking to other Docker users and contributors
@ -124,8 +124,8 @@ submitting a pull request.
Update the documentation when creating or modifying features. Test your Update the documentation when creating or modifying features. Test your
documentation changes for clarity, concision, and correctness, as well as a documentation changes for clarity, concision, and correctness, as well as a
clean documentation build. See our contributors guide for [our style clean documentation build. See our contributors guide for [our style
guide](https://docs.docker.com/contribute/style/grammar/) and instructions on [building guide](https://docs.docker.com/opensource/doc-style) and instructions on [building
the documentation](https://docs.docker.com/contribute/). the documentation](https://docs.docker.com/opensource/project/test-and-docs/#build-and-test-the-documentation).
Write clean code. Universally formatted code promotes ease of writing, reading, Write clean code. Universally formatted code promotes ease of writing, reading,
and maintenance. Always run `gofmt -s -w file.go` on each changed file before and maintenance. Always run `gofmt -s -w file.go` on each changed file before

View File

@ -4,7 +4,7 @@ ARG BASE_VARIANT=alpine
ARG ALPINE_VERSION=3.20 ARG ALPINE_VERSION=3.20
ARG BASE_DEBIAN_DISTRO=bookworm ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.23.2 ARG GO_VERSION=1.22.8
ARG XX_VERSION=1.5.0 ARG XX_VERSION=1.5.0
ARG GOVERSIONINFO_VERSION=v1.3.0 ARG GOVERSIONINFO_VERSION=v1.3.0
ARG GOTESTSUM_VERSION=v1.10.0 ARG GOTESTSUM_VERSION=v1.10.0

View File

@ -1,10 +1,9 @@
# Docker CLI # Docker CLI
[![PkgGoDev](https://pkg.go.dev/badge/github.com/docker/cli)](https://pkg.go.dev/github.com/docker/cli) [![PkgGoDev](https://img.shields.io/badge/go.dev-docs-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/docker/cli)
[![Build Status](https://img.shields.io/github/actions/workflow/status/docker/cli/build.yml?branch=master&label=build&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Abuild) [![Build Status](https://img.shields.io/github/actions/workflow/status/docker/cli/build.yml?branch=master&label=build&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Abuild)
[![Test Status](https://img.shields.io/github/actions/workflow/status/docker/cli/test.yml?branch=master&label=test&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Atest) [![Test Status](https://img.shields.io/github/actions/workflow/status/docker/cli/test.yml?branch=master&label=test&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Atest)
[![Go Report Card](https://goreportcard.com/badge/github.com/docker/cli)](https://goreportcard.com/report/github.com/docker/cli) [![Go Report Card](https://goreportcard.com/badge/github.com/docker/cli)](https://goreportcard.com/report/github.com/docker/cli)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/docker/cli/badge)](https://scorecard.dev/viewer/?uri=github.com/docker/cli)
[![Codecov](https://img.shields.io/codecov/c/github/docker/cli?logo=codecov)](https://codecov.io/gh/docker/cli) [![Codecov](https://img.shields.io/codecov/c/github/docker/cli?logo=codecov)](https://codecov.io/gh/docker/cli)
## About ## About

View File

@ -17,5 +17,5 @@ func (c *candidate) Path() string {
} }
func (c *candidate) Metadata() ([]byte, error) { func (c *candidate) Metadata() ([]byte, error) {
return exec.Command(c.path, MetadataSubcommandName).Output() // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments" return exec.Command(c.path, MetadataSubcommandName).Output()
} }

View File

@ -52,6 +52,7 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e
return return
} }
for _, p := range plugins { for _, p := range plugins {
p := p
vendor := p.Vendor vendor := p.Vendor
if vendor == "" { if vendor == "" {
vendor = "unknown" vendor = "unknown"

View File

@ -240,8 +240,7 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
// TODO: why are we not returning plugin.Err? // TODO: why are we not returning plugin.Err?
return nil, errPluginNotFound(name) return nil, errPluginNotFound(name)
} }
cmd := exec.Command(plugin.Path, args...) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments" cmd := exec.Command(plugin.Path, args...)
// Using dockerCli.{In,Out,Err}() here results in a hang until something is input. // Using dockerCli.{In,Out,Err}() here results in a hang until something is input.
// See: - https://github.com/golang/go/issues/10338 // See: - https://github.com/golang/go/issues/10338
// - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab // - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab

View File

@ -112,7 +112,7 @@ func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte,
return nil, wrapAsPluginError(err, "failed to marshall hook data") return nil, wrapAsPluginError(err, "failed to marshall hook data")
} }
pCmd := exec.CommandContext(ctx, p.Path, p.Name, HookSubcommandName, string(hDataBytes)) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments" pCmd := exec.CommandContext(ctx, p.Path, p.Name, HookSubcommandName, string(hDataBytes))
pCmd.Env = os.Environ() pCmd.Env = os.Environ()
pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0]) pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0])
hookCmdOutput, err := pCmd.Output() hookCmdOutput, err := pCmd.Output()

View File

@ -187,18 +187,19 @@ func TestInitializeFromClient(t *testing.T) {
}, },
} }
for _, tc := range testcases { for _, testcase := range testcases {
t.Run(tc.doc, func(t *testing.T) { testcase := testcase
t.Run(testcase.doc, func(t *testing.T) {
apiclient := &fakeClient{ apiclient := &fakeClient{
pingFunc: tc.pingFunc, pingFunc: testcase.pingFunc,
version: defaultVersion, version: defaultVersion,
} }
cli := &DockerCli{client: apiclient} cli := &DockerCli{client: apiclient}
err := cli.Initialize(flags.NewClientOptions()) err := cli.Initialize(flags.NewClientOptions())
assert.NilError(t, err) assert.NilError(t, err)
assert.DeepEqual(t, cli.ServerInfo(), tc.expectedServer) assert.DeepEqual(t, cli.ServerInfo(), testcase.expectedServer)
assert.Equal(t, apiclient.negotiated, tc.negotiated) assert.Equal(t, apiclient.negotiated, testcase.negotiated)
}) })
} }
} }
@ -276,9 +277,10 @@ func TestExperimentalCLI(t *testing.T) {
}, },
} }
for _, tc := range testcases { for _, testcase := range testcases {
t.Run(tc.doc, func(t *testing.T) { testcase := testcase
dir := fs.NewDir(t, tc.doc, fs.WithFile("config.json", tc.configfile)) t.Run(testcase.doc, func(t *testing.T) {
dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile))
defer dir.Remove() defer dir.Remove()
apiclient := &fakeClient{ apiclient := &fakeClient{
version: defaultVersion, version: defaultVersion,

View File

@ -59,7 +59,7 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
for _, ctr := range list { for _, ctr := range list {
skip := false skip := false
for _, fn := range filters { for _, fn := range filters {
if fn != nil && !fn(ctr) { if !fn(ctr) {
skip = true skip = true
break break
} }

View File

@ -1,346 +1,15 @@
package completion package completion
import ( import (
"context"
"errors"
"sort"
"testing" "testing"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/env"
) )
type fakeCLI struct {
*fakeClient
}
// Client implements [APIClientProvider].
func (c fakeCLI) Client() client.APIClient {
return c.fakeClient
}
type fakeClient struct {
client.Client
containerListFunc func(options container.ListOptions) ([]container.Summary, error)
imageListFunc func(options image.ListOptions) ([]image.Summary, error)
networkListFunc func(ctx context.Context, options network.ListOptions) ([]network.Summary, error)
volumeListFunc func(filter filters.Args) (volume.ListResponse, error)
}
func (c *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]container.Summary, error) {
if c.containerListFunc != nil {
return c.containerListFunc(options)
}
return []container.Summary{}, nil
}
func (c *fakeClient) ImageList(_ context.Context, options image.ListOptions) ([]image.Summary, error) {
if c.imageListFunc != nil {
return c.imageListFunc(options)
}
return []image.Summary{}, nil
}
func (c *fakeClient) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
if c.networkListFunc != nil {
return c.networkListFunc(ctx, options)
}
return []network.Inspect{}, nil
}
func (c *fakeClient) VolumeList(_ context.Context, options volume.ListOptions) (volume.ListResponse, error) {
if c.volumeListFunc != nil {
return c.volumeListFunc(options.Filters)
}
return volume.ListResponse{}, nil
}
func TestCompleteContainerNames(t *testing.T) {
tests := []struct {
doc string
showAll, showIDs bool
filters []func(container.Summary) bool
containers []container.Summary
expOut []string
expOpts container.ListOptions
expDirective cobra.ShellCompDirective
}{
{
doc: "no results",
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "all containers",
showAll: true,
containers: []container.Summary{
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
{ID: "id-a", State: "exited", Names: []string{"/container-a"}},
},
expOut: []string{"container-c", "container-c/link-b", "container-b", "container-a"},
expOpts: container.ListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "all containers with ids",
showAll: true,
showIDs: true,
containers: []container.Summary{
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
{ID: "id-a", State: "exited", Names: []string{"/container-a"}},
},
expOut: []string{"id-c", "container-c", "container-c/link-b", "id-b", "container-b", "id-a", "container-a"},
expOpts: container.ListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "only running containers",
showAll: false,
containers: []container.Summary{
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
},
expOut: []string{"container-c", "container-c/link-b"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with filter",
showAll: true,
filters: []func(container.Summary) bool{
func(container container.Summary) bool { return container.State == "created" },
},
containers: []container.Summary{
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
{ID: "id-a", State: "exited", Names: []string{"/container-a"}},
},
expOut: []string{"container-b"},
expOpts: container.ListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "multiple filters",
showAll: true,
filters: []func(container.Summary) bool{
func(container container.Summary) bool { return container.ID == "id-a" },
func(container container.Summary) bool { return container.State == "created" },
},
containers: []container.Summary{
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
{ID: "id-a", State: "created", Names: []string{"/container-a"}},
},
expOut: []string{"container-a"},
expOpts: container.ListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with error",
expDirective: cobra.ShellCompDirectiveError,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
if tc.showIDs {
t.Setenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS", "yes")
}
comp := ContainerNames(fakeCLI{&fakeClient{
containerListFunc: func(opts container.ListOptions) ([]container.Summary, error) {
assert.Check(t, is.DeepEqual(opts, tc.expOpts, cmpopts.IgnoreUnexported(container.ListOptions{}, filters.Args{})))
if tc.expDirective == cobra.ShellCompDirectiveError {
return nil, errors.New("some error occurred")
}
return tc.containers, nil
},
}}, tc.showAll, tc.filters...)
containers, directives := comp(&cobra.Command{}, nil, "")
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
assert.Check(t, is.DeepEqual(containers, tc.expOut))
})
}
}
func TestCompleteEnvVarNames(t *testing.T) {
env.PatchAll(t, map[string]string{
"ENV_A": "hello-a",
"ENV_B": "hello-b",
})
values, directives := EnvVarNames(nil, nil, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
sort.Strings(values)
expected := []string{"ENV_A", "ENV_B"}
assert.Check(t, is.DeepEqual(values, expected))
}
func TestCompleteFileNames(t *testing.T) {
values, directives := FileNames(nil, nil, "")
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveDefault))
assert.Check(t, is.Len(values, 0))
}
func TestCompleteFromList(t *testing.T) {
expected := []string{"one", "two", "three"}
values, directives := FromList(expected...)(nil, nil, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
assert.Check(t, is.DeepEqual(values, expected))
}
func TestCompleteImageNames(t *testing.T) {
tests := []struct {
doc string
images []image.Summary
expOut []string
expDirective cobra.ShellCompDirective
}{
{
doc: "no results",
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with results",
images: []image.Summary{
{RepoTags: []string{"image-c:latest", "image-c:other"}},
{RepoTags: []string{"image-b:latest", "image-b:other"}},
{RepoTags: []string{"image-a:latest", "image-a:other"}},
},
expOut: []string{"image-c:latest", "image-c:other", "image-b:latest", "image-b:other", "image-a:latest", "image-a:other"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with error",
expDirective: cobra.ShellCompDirectiveError,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
comp := ImageNames(fakeCLI{&fakeClient{
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
if tc.expDirective == cobra.ShellCompDirectiveError {
return nil, errors.New("some error occurred")
}
return tc.images, nil
},
}})
volumes, directives := comp(&cobra.Command{}, nil, "")
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
})
}
}
func TestCompleteNetworkNames(t *testing.T) {
tests := []struct {
doc string
networks []network.Summary
expOut []string
expDirective cobra.ShellCompDirective
}{
{
doc: "no results",
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with results",
networks: []network.Summary{
{ID: "nw-c", Name: "network-c"},
{ID: "nw-b", Name: "network-b"},
{ID: "nw-a", Name: "network-a"},
},
expOut: []string{"network-c", "network-b", "network-a"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with error",
expDirective: cobra.ShellCompDirectiveError,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
comp := NetworkNames(fakeCLI{&fakeClient{
networkListFunc: func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
if tc.expDirective == cobra.ShellCompDirectiveError {
return nil, errors.New("some error occurred")
}
return tc.networks, nil
},
}})
volumes, directives := comp(&cobra.Command{}, nil, "")
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
})
}
}
func TestCompleteNoComplete(t *testing.T) {
values, directives := NoComplete(nil, nil, "")
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveNoFileComp))
assert.Check(t, is.Len(values, 0))
}
func TestCompletePlatforms(t *testing.T) { func TestCompletePlatforms(t *testing.T) {
values, directives := Platforms(nil, nil, "") values, directives := Platforms(nil, nil, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion") assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
assert.Check(t, is.DeepEqual(values, commonPlatforms)) assert.Check(t, is.DeepEqual(values, commonPlatforms))
} }
func TestCompleteVolumeNames(t *testing.T) {
tests := []struct {
doc string
volumes []*volume.Volume
expOut []string
expDirective cobra.ShellCompDirective
}{
{
doc: "no results",
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with results",
volumes: []*volume.Volume{
{Name: "volume-c"},
{Name: "volume-b"},
{Name: "volume-a"},
},
expOut: []string{"volume-c", "volume-b", "volume-a"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with error",
expDirective: cobra.ShellCompDirectiveError,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
comp := VolumeNames(fakeCLI{&fakeClient{
volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) {
if tc.expDirective == cobra.ShellCompDirectiveError {
return volume.ListResponse{}, errors.New("some error occurred")
}
return volume.ListResponse{Volumes: tc.volumes}, nil
},
}})
volumes, directives := comp(&cobra.Command{}, nil, "")
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
})
}
}

View File

@ -43,6 +43,7 @@ func TestConfigCreateErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.expectedError, func(t *testing.T) { t.Run(tc.expectedError, func(t *testing.T) {
cmd := newConfigCreateCommand( cmd := newConfigCreateCommand(
test.NewFakeCli(&fakeClient{ test.NewFakeCli(&fakeClient{

View File

@ -61,6 +61,7 @@ id_rsa
}, },
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out

View File

@ -73,6 +73,7 @@ func TestNewAttachCommandErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})) cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)

View File

@ -178,6 +178,7 @@ func TestSplitCpArg(t *testing.T) {
}, },
} }
for _, tc := range testcases { for _, tc := range testcases {
tc := tc
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
if tc.os == "windows" && runtime.GOOS != "windows" { if tc.os == "windows" && runtime.GOOS != "windows" {
t.Skip("skipping windows test on non-windows platform") t.Skip("skipping windows test on non-windows platform")

View File

@ -113,6 +113,7 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
}, },
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(tc.PullPolicy, func(t *testing.T) { t.Run(tc.PullPolicy, func(t *testing.T) {
pullCounter := 0 pullCounter := 0
@ -175,6 +176,7 @@ func TestCreateContainerImagePullPolicyInvalid(t *testing.T) {
}, },
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(tc.PullPolicy, func(t *testing.T) { t.Run(tc.PullPolicy, func(t *testing.T) {
dockerCli := test.NewFakeCli(&fakeClient{}) dockerCli := test.NewFakeCli(&fakeClient{})
err := runCreate( err := runCreate(
@ -205,6 +207,7 @@ func TestCreateContainerValidateFlags(t *testing.T) {
expectedErr: `invalid argument "STDINFO" for "-a, --attach" flag: valid streams are STDIN, STDOUT and STDERR`, expectedErr: `invalid argument "STDINFO" for "-a, --attach" flag: valid streams are STDIN, STDOUT and STDERR`,
}, },
} { } {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := NewCreateCommand(test.NewFakeCli(&fakeClient{})) cmd := NewCreateCommand(test.NewFakeCli(&fakeClient{}))
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
@ -248,6 +251,7 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
fakeCLI := test.NewFakeCli(&fakeClient{ fakeCLI := test.NewFakeCli(&fakeClient{
createContainerFunc: func(config *container.Config, createContainerFunc: func(config *container.Config,
hostConfig *container.HostConfig, hostConfig *container.HostConfig,
@ -308,6 +312,7 @@ func TestNewCreateCommandWithWarnings(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
fakeCLI := test.NewFakeCli(&fakeClient{ fakeCLI := test.NewFakeCli(&fakeClient{
createContainerFunc: func(config *container.Config, createContainerFunc: func(config *container.Config,

View File

@ -47,6 +47,7 @@ D: /usr/app/old_app.js
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
tc.context.Output = out tc.context.Output = out

View File

@ -178,6 +178,7 @@ container2 -- --
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out
@ -222,6 +223,7 @@ func TestContainerStatsContextWriteWithNoStats(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
err := statsFormatWrite(tc.context, []StatsEntry{}, "linux", false) err := statsFormatWrite(tc.context, []StatsEntry{}, "linux", false)
assert.NilError(t, err) assert.NilError(t, err)
@ -263,6 +265,7 @@ func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
err := statsFormatWrite(tc.context, []StatsEntry{}, "windows", false) err := statsFormatWrite(tc.context, []StatsEntry{}, "windows", false)
assert.NilError(t, err) assert.NilError(t, err)

View File

@ -127,6 +127,7 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
func TestContainerListErrors(t *testing.T) { func TestContainerListErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string
flags map[string]string flags map[string]string
containerListFunc func(container.ListOptions) ([]container.Summary, error) containerListFunc func(container.ListOptions) ([]container.Summary, error)
expectedError string expectedError string
@ -156,10 +157,10 @@ func TestContainerListErrors(t *testing.T) {
containerListFunc: tc.containerListFunc, containerListFunc: tc.containerListFunc,
}), }),
) )
cmd.SetArgs(tc.args)
for key, value := range tc.flags { for key, value := range tc.flags {
assert.Check(t, cmd.Flags().Set(key, value)) assert.Check(t, cmd.Flags().Set(key, value))
} }
cmd.SetArgs([]string{})
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard) cmd.SetErr(io.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError) assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -179,9 +180,6 @@ func TestContainerListWithoutFormat(t *testing.T) {
}, },
}) })
cmd := newListCommand(cli) cmd := newListCommand(cli)
cmd.SetArgs([]string{})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format.golden") golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format.golden")
} }
@ -196,9 +194,6 @@ func TestContainerListNoTrunc(t *testing.T) {
}, },
}) })
cmd := newListCommand(cli) cmd := newListCommand(cli)
cmd.SetArgs([]string{})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.Check(t, cmd.Flags().Set("no-trunc", "true")) assert.Check(t, cmd.Flags().Set("no-trunc", "true"))
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format-no-trunc.golden") golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format-no-trunc.golden")
@ -215,9 +210,6 @@ func TestContainerListNamesMultipleTime(t *testing.T) {
}, },
}) })
cmd := newListCommand(cli) cmd := newListCommand(cli)
cmd.SetArgs([]string{})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.Check(t, cmd.Flags().Set("format", "{{.Names}} {{.Names}}")) assert.Check(t, cmd.Flags().Set("format", "{{.Names}} {{.Names}}"))
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "container-list-format-name-name.golden") golden.Assert(t, cli.OutBuffer().String(), "container-list-format-name-name.golden")
@ -234,9 +226,6 @@ func TestContainerListFormatTemplateWithArg(t *testing.T) {
}, },
}) })
cmd := newListCommand(cli) cmd := newListCommand(cli)
cmd.SetArgs([]string{})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.Check(t, cmd.Flags().Set("format", `{{.Names}} {{.Label "some.label"}}`)) assert.Check(t, cmd.Flags().Set("format", `{{.Names}} {{.Label "some.label"}}`))
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "container-list-format-with-arg.golden") golden.Assert(t, cli.OutBuffer().String(), "container-list-format-with-arg.golden")
@ -277,6 +266,7 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) {
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(options container.ListOptions) ([]container.Summary, error) { containerListFunc: func(options container.ListOptions) ([]container.Summary, error) {
@ -285,9 +275,6 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) {
}, },
}) })
cmd := newListCommand(cli) cmd := newListCommand(cli)
cmd.SetArgs([]string{})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.Check(t, cmd.Flags().Set("format", tc.format)) assert.Check(t, cmd.Flags().Set("format", tc.format))
if tc.sizeFlag != "" { if tc.sizeFlag != "" {
assert.Check(t, cmd.Flags().Set("size", tc.sizeFlag)) assert.Check(t, cmd.Flags().Set("size", tc.sizeFlag))
@ -310,9 +297,6 @@ func TestContainerListWithConfigFormat(t *testing.T) {
PsFormat: "{{ .Names }} {{ .Image }} {{ .Labels }} {{ .Size}}", PsFormat: "{{ .Names }} {{ .Image }} {{ .Labels }} {{ .Size}}",
}) })
cmd := newListCommand(cli) cmd := newListCommand(cli)
cmd.SetArgs([]string{})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "container-list-with-config-format.golden") golden.Assert(t, cli.OutBuffer().String(), "container-list-with-config-format.golden")
} }
@ -330,9 +314,6 @@ func TestContainerListWithFormat(t *testing.T) {
t.Run("with format", func(t *testing.T) { t.Run("with format", func(t *testing.T) {
cli.OutBuffer().Reset() cli.OutBuffer().Reset()
cmd := newListCommand(cli) cmd := newListCommand(cli)
cmd.SetArgs([]string{})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.Check(t, cmd.Flags().Set("format", "{{ .Names }} {{ .Image }} {{ .Labels }}")) assert.Check(t, cmd.Flags().Set("format", "{{ .Names }} {{ .Image }} {{ .Labels }}"))
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "container-list-with-format.golden") golden.Assert(t, cli.OutBuffer().String(), "container-list-with-format.golden")
@ -341,9 +322,6 @@ func TestContainerListWithFormat(t *testing.T) {
t.Run("with format and quiet", func(t *testing.T) { t.Run("with format and quiet", func(t *testing.T) {
cli.OutBuffer().Reset() cli.OutBuffer().Reset()
cmd := newListCommand(cli) cmd := newListCommand(cli)
cmd.SetArgs([]string{})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.Check(t, cmd.Flags().Set("format", "{{ .Names }} {{ .Image }} {{ .Labels }}")) assert.Check(t, cmd.Flags().Set("format", "{{ .Names }} {{ .Image }} {{ .Labels }}"))
assert.Check(t, cmd.Flags().Set("quiet", "true")) assert.Check(t, cmd.Flags().Set("quiet", "true"))
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())

View File

@ -23,6 +23,7 @@ import (
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/pflag" "github.com/spf13/pflag"
cdi "tags.cncf.io/container-device-interface/pkg/parser" cdi "tags.cncf.io/container-device-interface/pkg/parser"
) )
@ -363,6 +364,10 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
return nil, errors.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) return nil, errors.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness)
} }
mounts := copts.mounts.Value()
if len(mounts) > 0 && copts.volumeDriver != "" {
logrus.Warn("`--volume-driver` is ignored for volumes specified via `--mount`. Use `--mount type=volume,volume-driver=...` instead.")
}
var binds []string var binds []string
volumes := copts.volumes.GetMap() volumes := copts.volumes.GetMap()
// add any bind targets to the list of container volumes // add any bind targets to the list of container volumes
@ -692,7 +697,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
Tmpfs: tmpfs, Tmpfs: tmpfs,
Sysctls: copts.sysctls.GetAll(), Sysctls: copts.sysctls.GetAll(),
Runtime: copts.runtime, Runtime: copts.runtime,
Mounts: copts.mounts.Value(), Mounts: mounts,
MaskedPaths: maskedPaths, MaskedPaths: maskedPaths,
ReadonlyPaths: readonlyPaths, ReadonlyPaths: readonlyPaths,
Annotations: copts.annotations.GetAll(), Annotations: copts.annotations.GetAll(),
@ -762,6 +767,7 @@ func parseNetworkOpts(copts *containerOptions) (map[string]*networktypes.Endpoin
} }
for i, n := range copts.netMode.Value() { for i, n := range copts.netMode.Value() {
n := n
if container.NetworkMode(n.Target).IsUserDefined() { if container.NetworkMode(n.Target).IsUserDefined() {
hasUserDefined = true hasUserDefined = true
} else { } else {

View File

@ -126,6 +126,7 @@ func TestParseRunAttach(t *testing.T) {
}, },
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc
t.Run(tc.input, func(t *testing.T) { t.Run(tc.input, func(t *testing.T) {
config, _, _ := mustParse(t, tc.input) config, _, _ := mustParse(t, tc.input)
assert.Equal(t, config.AttachStdin, tc.expected.AttachStdin) assert.Equal(t, config.AttachStdin, tc.expected.AttachStdin)
@ -801,6 +802,7 @@ func TestParseRestartPolicy(t *testing.T) {
}, },
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc
t.Run(tc.input, func(t *testing.T) { t.Run(tc.input, func(t *testing.T) {
_, hostConfig, _, err := parseRun([]string{"--restart=" + tc.input, "img", "cmd"}) _, hostConfig, _, err := parseRun([]string{"--restart=" + tc.input, "img", "cmd"})
if tc.expectedErr != "" { if tc.expectedErr != "" {

View File

@ -43,6 +43,7 @@ func TestNewPortCommandOutput(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
inspectFunc: func(string) (container.InspectResponse, error) { inspectFunc: func(string) (container.InspectResponse, error) {

View File

@ -21,7 +21,6 @@ func TestContainerPrunePromptTermination(t *testing.T) {
}, },
}) })
cmd := NewPruneCommand(cli) cmd := NewPruneCommand(cli)
cmd.SetArgs([]string{})
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard) cmd.SetErr(io.Discard)
test.TerminatePrompt(ctx, t, cmd, cli) test.TerminatePrompt(ctx, t, cmd, cli)

View File

@ -58,6 +58,7 @@ func TestRestart(t *testing.T) {
expectedErr: "conflicting options: cannot specify both --timeout and --time", expectedErr: "conflicting options: cannot specify both --timeout and --time",
}, },
} { } {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
var restarted []string var restarted []string
mutex := new(sync.Mutex) mutex := new(sync.Mutex)

View File

@ -38,9 +38,7 @@ func NewRmCommand(dockerCli command.Cli) *cobra.Command {
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container rm, docker container remove, docker rm", "aliases": "docker container rm, docker container remove, docker rm",
}, },
ValidArgsFunction: completion.ContainerNames(dockerCli, true, func(ctr container.Summary) bool { ValidArgsFunction: completion.ContainerNames(dockerCli, true),
return opts.force || ctr.State == "exited" || ctr.State == "created"
}),
} }
flags := cmd.Flags() flags := cmd.Flags()

View File

@ -23,6 +23,7 @@ func TestRemoveForce(t *testing.T) {
{name: "without force", args: []string{"nosuchcontainer", "mycontainer"}, expectedErr: "no such container"}, {name: "without force", args: []string{"nosuchcontainer", "mycontainer"}, expectedErr: "no such container"},
{name: "with force", args: []string{"--force", "nosuchcontainer", "mycontainer"}, expectedErr: ""}, {name: "with force", args: []string{"--force", "nosuchcontainer", "mycontainer"}, expectedErr: ""},
} { } {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
var removed []string var removed []string
mutex := new(sync.Mutex) mutex := new(sync.Mutex)

View File

@ -35,6 +35,7 @@ func TestRunValidateFlags(t *testing.T) {
expectedErr: "conflicting options: cannot specify both --attach and --detach", expectedErr: "conflicting options: cannot specify both --attach and --detach",
}, },
} { } {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := NewRunCommand(test.NewFakeCli(&fakeClient{})) cmd := NewRunCommand(test.NewFakeCli(&fakeClient{}))
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
@ -244,6 +245,7 @@ func TestRunCommandWithContentTrustErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
fakeCLI := test.NewFakeCli(&fakeClient{ fakeCLI := test.NewFakeCli(&fakeClient{
createContainerFunc: func(config *container.Config, createContainerFunc: func(config *container.Config,
@ -284,6 +286,7 @@ func TestRunContainerImagePullPolicyInvalid(t *testing.T) {
}, },
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(tc.PullPolicy, func(t *testing.T) { t.Run(tc.PullPolicy, func(t *testing.T) {
dockerCli := test.NewFakeCli(&fakeClient{}) dockerCli := test.NewFakeCli(&fakeClient{})
err := runRun( err := runRun(

View File

@ -1,7 +1,6 @@
package container package container
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
@ -265,40 +264,31 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
// so we unlikely hit this code in practice. // so we unlikely hit this code in practice.
daemonOSType = dockerCLI.ServerInfo().OSType daemonOSType = dockerCLI.ServerInfo().OSType
} }
// Buffer to store formatted stats text.
// Once formatted, it will be printed in one write to avoid screen flickering.
var statsTextBuffer bytes.Buffer
statsCtx := formatter.Context{ statsCtx := formatter.Context{
Output: &statsTextBuffer, Output: dockerCLI.Out(),
Format: NewStatsFormat(format, daemonOSType), Format: NewStatsFormat(format, daemonOSType),
} }
cleanScreen := func() {
if !options.NoStream {
_, _ = fmt.Fprint(dockerCLI.Out(), "\033[2J")
_, _ = fmt.Fprint(dockerCLI.Out(), "\033[H")
}
}
var err error var err error
ticker := time.NewTicker(500 * time.Millisecond) ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for range ticker.C {
cleanScreen()
var ccStats []StatsEntry var ccStats []StatsEntry
cStats.mu.RLock() cStats.mu.RLock()
for _, c := range cStats.cs { for _, c := range cStats.cs {
ccStats = append(ccStats, c.GetStatistics()) ccStats = append(ccStats, c.GetStatistics())
} }
cStats.mu.RUnlock() cStats.mu.RUnlock()
if !options.NoStream {
// Start by clearing the screen and moving the cursor to the top-left
_, _ = fmt.Fprint(&statsTextBuffer, "\033[2J\033[H")
}
if err = statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil { if err = statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
break break
} }
_, _ = fmt.Fprint(dockerCLI.Out(), statsTextBuffer.String())
statsTextBuffer.Reset()
if len(cStats.cs) == 0 && !showAll { if len(cStats.cs) == 0 && !showAll {
break break
} }

View File

@ -58,6 +58,7 @@ func TestStop(t *testing.T) {
expectedErr: "conflicting options: cannot specify both --timeout and --time", expectedErr: "conflicting options: cannot specify both --timeout and --time",
}, },
} { } {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
var stopped []string var stopped []string
mutex := new(sync.Mutex) mutex := new(sync.Mutex)

View File

@ -38,7 +38,7 @@ func waitFn(cid string) (<-chan container.WaitResponse, <-chan error) {
} }
func TestWaitExitOrRemoved(t *testing.T) { func TestWaitExitOrRemoved(t *testing.T) {
tests := []struct { testcases := []struct {
cid string cid string
exitCode int exitCode int
}{ }{
@ -61,11 +61,9 @@ func TestWaitExitOrRemoved(t *testing.T) {
} }
client := &fakeClient{waitFunc: waitFn, Version: api.DefaultVersion} client := &fakeClient{waitFunc: waitFn, Version: api.DefaultVersion}
for _, tc := range tests { for _, testcase := range testcases {
t.Run(tc.cid, func(t *testing.T) { statusC := waitExitOrRemoved(context.Background(), client, testcase.cid, true)
statusC := waitExitOrRemoved(context.Background(), client, tc.cid, true) exitCode := <-statusC
exitCode := <-statusC assert.Check(t, is.Equal(testcase.exitCode, exitCode))
assert.Check(t, is.Equal(tc.exitCode, exitCode))
})
} }
} }

View File

@ -94,6 +94,7 @@ func TestCreate(t *testing.T) {
}, },
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc
t.Run(tc.options.Name, func(t *testing.T) { t.Run(tc.options.Name, func(t *testing.T) {
err := RunCreate(cli, &tc.options) err := RunCreate(cli, &tc.options)
if tc.expecterErr == "" { if tc.expecterErr == "" {
@ -163,24 +164,25 @@ func TestCreateFromContext(t *testing.T) {
cli.SetCurrentContext("dummy") cli.SetCurrentContext("dummy")
for _, tc := range cases { for _, c := range cases {
t.Run(tc.name, func(t *testing.T) { c := c
t.Run(c.name, func(t *testing.T) {
cli.ResetOutputBuffers() cli.ResetOutputBuffers()
err := RunCreate(cli, &CreateOptions{ err := RunCreate(cli, &CreateOptions{
From: "original", From: "original",
Name: tc.name, Name: c.name,
Description: tc.description, Description: c.description,
Docker: tc.docker, Docker: c.docker,
}) })
assert.NilError(t, err) assert.NilError(t, err)
assertContextCreateLogging(t, cli, tc.name) assertContextCreateLogging(t, cli, c.name)
newContext, err := cli.ContextStore().GetMetadata(tc.name) newContext, err := cli.ContextStore().GetMetadata(c.name)
assert.NilError(t, err) assert.NilError(t, err)
newContextTyped, err := command.GetDockerContext(newContext) newContextTyped, err := command.GetDockerContext(newContext)
assert.NilError(t, err) assert.NilError(t, err)
dockerEndpoint, err := docker.EndpointFromContext(newContext) dockerEndpoint, err := docker.EndpointFromContext(newContext)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, newContextTyped.Description, tc.expectedDescription) assert.Equal(t, newContextTyped.Description, c.expectedDescription)
assert.Equal(t, dockerEndpoint.Host, "tcp://42.42.42.42:2375") assert.Equal(t, dockerEndpoint.Host, "tcp://42.42.42.42:2375")
}) })
} }
@ -217,22 +219,23 @@ func TestCreateFromCurrent(t *testing.T) {
cli.SetCurrentContext("original") cli.SetCurrentContext("original")
for _, tc := range cases { for _, c := range cases {
t.Run(tc.name, func(t *testing.T) { c := c
t.Run(c.name, func(t *testing.T) {
cli.ResetOutputBuffers() cli.ResetOutputBuffers()
err := RunCreate(cli, &CreateOptions{ err := RunCreate(cli, &CreateOptions{
Name: tc.name, Name: c.name,
Description: tc.description, Description: c.description,
}) })
assert.NilError(t, err) assert.NilError(t, err)
assertContextCreateLogging(t, cli, tc.name) assertContextCreateLogging(t, cli, c.name)
newContext, err := cli.ContextStore().GetMetadata(tc.name) newContext, err := cli.ContextStore().GetMetadata(c.name)
assert.NilError(t, err) assert.NilError(t, err)
newContextTyped, err := command.GetDockerContext(newContext) newContextTyped, err := command.GetDockerContext(newContext)
assert.NilError(t, err) assert.NilError(t, err)
dockerEndpoint, err := docker.EndpointFromContext(newContext) dockerEndpoint, err := docker.EndpointFromContext(newContext)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, newContextTyped.Description, tc.expectedDescription) assert.Equal(t, newContextTyped.Description, c.expectedDescription)
assert.Equal(t, dockerEndpoint.Host, "tcp://42.42.42.42:2375") assert.Equal(t, dockerEndpoint.Host, "tcp://42.42.42.42:2375")
}) })
} }

View File

@ -346,6 +346,7 @@ size: 0B
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out
@ -410,6 +411,7 @@ func TestContainerContextWriteWithNoContainers(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
err := ContainerWrite(tc.context, containers) err := ContainerWrite(tc.context, containers)
assert.NilError(t, err) assert.NilError(t, err)

View File

@ -106,6 +106,7 @@ Build Cache 0 0 0B 0B
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out

View File

@ -309,6 +309,7 @@ image_id: imageID3
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out
@ -369,6 +370,7 @@ func TestImageContextWriteWithNoImage(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
err := ImageWrite(tc.context, images) err := ImageWrite(tc.context, images)
assert.NilError(t, err) assert.NilError(t, err)

View File

@ -131,6 +131,7 @@ foobar_bar
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out

View File

@ -255,6 +255,7 @@ imageID6 17 years ago /bin/bash echo 183MB
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
err := HistoryWrite(tc.context, true, histories) err := HistoryWrite(tc.context, true, histories)
assert.NilError(t, err) assert.NilError(t, err)

View File

@ -3,20 +3,17 @@ package image
import ( import (
"context" "context"
"github.com/containerd/platforms"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
flagsHelper "github.com/docker/cli/cli/flags" flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type historyOptions struct { type historyOptions struct {
image string image string
platform string
human bool human bool
quiet bool quiet bool
@ -48,24 +45,12 @@ func NewHistoryCommand(dockerCli command.Cli) *cobra.Command {
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show image IDs") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show image IDs")
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output")
flags.StringVar(&opts.format, "format", "", flagsHelper.FormatHelp) flags.StringVar(&opts.format, "format", "", flagsHelper.FormatHelp)
flags.StringVar(&opts.platform, "platform", "", `Show history for the given platform. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`)
_ = flags.SetAnnotation("platform", "version", []string{"1.48"})
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
return cmd return cmd
} }
func runHistory(ctx context.Context, dockerCli command.Cli, opts historyOptions) error { func runHistory(ctx context.Context, dockerCli command.Cli, opts historyOptions) error {
var options image.HistoryOptions history, err := dockerCli.Client().ImageHistory(ctx, opts.image, image.HistoryOptions{})
if opts.platform != "" {
p, err := platforms.Parse(opts.platform)
if err != nil {
return errors.Wrap(err, "invalid platform")
}
options.Platform = &p
}
history, err := dockerCli.Client().ImageHistory(ctx, opts.image, options)
if err != nil { if err != nil {
return err return err
} }

View File

@ -8,10 +8,8 @@ import (
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
) )
@ -35,13 +33,9 @@ func TestNewHistoryCommandErrors(t *testing.T) {
return []image.HistoryResponseItem{{}}, errors.Errorf("something went wrong") return []image.HistoryResponseItem{{}}, errors.Errorf("something went wrong")
}, },
}, },
{
name: "invalid platform",
args: []string{"--platform", "<invalid>", "arg1"},
expectedError: `invalid platform`,
},
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc})) cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}))
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
@ -95,19 +89,9 @@ func TestNewHistoryCommandSuccess(t *testing.T) {
}}, nil }}, nil
}, },
}, },
{
name: "platform",
args: []string{"--platform", "linux/amd64", "image:tag"},
imageHistoryFunc: func(img string, options image.HistoryOptions) ([]image.HistoryResponseItem, error) {
assert.Check(t, is.DeepEqual(ocispec.Platform{OS: "linux", Architecture: "amd64"}, *options.Platform))
return []image.HistoryResponseItem{{
ID: "1234567890123456789",
Created: time.Now().Unix(),
}}, nil
},
},
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
// Set to UTC timezone as timestamps in output are // Set to UTC timezone as timestamps in output are
// printed in the current timezone // printed in the current timezone

View File

@ -98,6 +98,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc})) cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}))
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)

View File

@ -25,6 +25,7 @@ func TestNewInspectCommandErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := newInspectCommand(test.NewFakeCli(&fakeClient{})) cmd := newInspectCommand(test.NewFakeCli(&fakeClient{}))
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
@ -78,6 +79,7 @@ func TestNewInspectCommandSuccess(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
imageInspectInvocationCount = 0 imageInspectInvocationCount = 0
cli := test.NewFakeCli(&fakeClient{imageInspectFunc: tc.imageInspectFunc}) cli := test.NewFakeCli(&fakeClient{imageInspectFunc: tc.imageInspectFunc})

View File

@ -35,6 +35,7 @@ func TestNewImagesCommandErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := NewImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc})) cmd := NewImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}))
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
@ -82,6 +83,7 @@ func TestNewImagesCommandSuccess(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}) cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc})
cli.SetConfigFile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat}) cli.SetConfigFile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat})

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"io" "io"
"github.com/containerd/platforms"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
@ -16,9 +15,8 @@ import (
) )
type loadOptions struct { type loadOptions struct {
input string input string
quiet bool quiet bool
platform string
} }
// NewLoadCommand creates a new `docker load` command // NewLoadCommand creates a new `docker load` command
@ -42,10 +40,7 @@ func NewLoadCommand(dockerCli command.Cli) *cobra.Command {
flags.StringVarP(&opts.input, "input", "i", "", "Read from tar archive file, instead of STDIN") flags.StringVarP(&opts.input, "input", "i", "", "Read from tar archive file, instead of STDIN")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the load output") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the load output")
flags.StringVar(&opts.platform, "platform", "", `Load only the given platform variant. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`)
_ = flags.SetAnnotation("platform", "version", []string{"1.48"})
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
return cmd return cmd
} }
@ -68,20 +63,12 @@ func runLoad(ctx context.Context, dockerCli command.Cli, opts loadOptions) error
return errors.Errorf("requested load from stdin, but stdin is empty") return errors.Errorf("requested load from stdin, but stdin is empty")
} }
var options image.LoadOptions var loadOpts image.LoadOptions
if opts.quiet || !dockerCli.Out().IsTerminal() { if opts.quiet || !dockerCli.Out().IsTerminal() {
options.Quiet = true loadOpts.Quiet = true
} }
if opts.platform != "" { response, err := dockerCli.Client().ImageLoad(ctx, input, loadOpts)
p, err := platforms.Parse(opts.platform)
if err != nil {
return errors.Wrap(err, "invalid platform")
}
options.Platform = &p
}
response, err := dockerCli.Client().ImageLoad(ctx, input, options)
if err != nil { if err != nil {
return err return err
} }

View File

@ -8,10 +8,8 @@ import (
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
) )
@ -30,28 +28,19 @@ func TestNewLoadCommandErrors(t *testing.T) {
}, },
{ {
name: "input-to-terminal", name: "input-to-terminal",
args: []string{},
isTerminalIn: true, isTerminalIn: true,
expectedError: "requested load from stdin, but stdin is empty", expectedError: "requested load from stdin, but stdin is empty",
}, },
{ {
name: "pull-error", name: "pull-error",
args: []string{},
expectedError: "something went wrong", expectedError: "something went wrong",
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) { imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
return image.LoadResponse{}, errors.Errorf("something went wrong") return image.LoadResponse{}, errors.Errorf("something went wrong")
}, },
}, },
{
name: "invalid platform",
args: []string{"--platform", "<invalid>"},
expectedError: `invalid platform`,
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
return image.LoadResponse{}, nil
},
},
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}) cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc})
cli.In().SetIsTerminal(tc.isTerminalIn) cli.In().SetIsTerminal(tc.isTerminalIn)
@ -82,14 +71,12 @@ func TestNewLoadCommandSuccess(t *testing.T) {
}{ }{
{ {
name: "simple", name: "simple",
args: []string{},
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) { imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil
}, },
}, },
{ {
name: "json", name: "json",
args: []string{},
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) { imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
json := "{\"ID\": \"1\"}" json := "{\"ID\": \"1\"}"
return image.LoadResponse{ return image.LoadResponse{
@ -105,16 +92,9 @@ func TestNewLoadCommandSuccess(t *testing.T) {
return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil
}, },
}, },
{
name: "with platform",
args: []string{"--platform", "linux/amd64"},
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
assert.Check(t, is.DeepEqual(ocispec.Platform{OS: "linux", Architecture: "amd64"}, *options.Platform))
return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil
},
},
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}) cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc})
cmd := NewLoadCommand(cli) cmd := NewLoadCommand(cli)

View File

@ -39,6 +39,7 @@ func TestNewPruneCommandErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{ cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{
imagesPruneFunc: tc.imagesPruneFunc, imagesPruneFunc: tc.imagesPruneFunc,
@ -97,6 +98,7 @@ func TestNewPruneCommandSuccess(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc}) cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc})
// when prompted, answer "Y" to confirm the prune. // when prompted, answer "Y" to confirm the prune.

View File

@ -38,6 +38,7 @@ func TestNewPullCommandErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{}) cli := test.NewFakeCli(&fakeClient{})
cmd := NewPullCommand(cli) cmd := NewPullCommand(cli)
@ -72,6 +73,7 @@ func TestNewPullCommandSuccess(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) { imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) {
@ -117,6 +119,7 @@ func TestNewPullCommandWithContentTrustErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) { imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) {

View File

@ -38,6 +38,7 @@ func TestNewPushCommandErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc}) cli := test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc})
cmd := NewPushCommand(cli) cmd := NewPushCommand(cli)
@ -67,6 +68,7 @@ func TestNewPushCommandSuccess(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
imagePushFunc: func(ref string, options image.PushOptions) (io.ReadCloser, error) { imagePushFunc: func(ref string, options image.PushOptions) (io.ReadCloser, error) {

View File

@ -62,6 +62,7 @@ func TestNewRemoveCommandErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{ cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{
imageRemoveFunc: tc.imageRemoveFunc, imageRemoveFunc: tc.imageRemoveFunc,
@ -120,6 +121,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc}) cli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc})
cmd := NewRemoveCommand(cli) cmd := NewRemoveCommand(cli)

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"io" "io"
"github.com/containerd/platforms"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
@ -14,9 +13,8 @@ import (
) )
type saveOptions struct { type saveOptions struct {
images []string images []string
output string output string
platform string
} }
// NewSaveCommand creates a new `docker save` command // NewSaveCommand creates a new `docker save` command
@ -40,10 +38,7 @@ func NewSaveCommand(dockerCli command.Cli) *cobra.Command {
flags := cmd.Flags() flags := cmd.Flags()
flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT") flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT")
flags.StringVar(&opts.platform, "platform", "", `Save only the given platform variant. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`)
_ = flags.SetAnnotation("platform", "version", []string{"1.48"})
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
return cmd return cmd
} }
@ -57,16 +52,7 @@ func RunSave(ctx context.Context, dockerCli command.Cli, opts saveOptions) error
return errors.Wrap(err, "failed to save image") return errors.Wrap(err, "failed to save image")
} }
var options image.SaveOptions responseBody, err := dockerCli.Client().ImageSave(ctx, opts.images, image.SaveOptions{})
if opts.platform != "" {
p, err := platforms.Parse(opts.platform)
if err != nil {
return errors.Wrap(err, "invalid platform")
}
options.Platform = &p
}
responseBody, err := dockerCli.Client().ImageSave(ctx, opts.images, options)
if err != nil { if err != nil {
return err return err
} }

View File

@ -8,7 +8,6 @@ import (
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
@ -52,13 +51,9 @@ func TestNewSaveCommandErrors(t *testing.T) {
args: []string{"-o", "/dev/null", "arg1"}, args: []string{"-o", "/dev/null", "arg1"},
expectedError: "failed to save image: invalid output path: \"/dev/null\" must be a directory or a regular file", expectedError: "failed to save image: invalid output path: \"/dev/null\" must be a directory or a regular file",
}, },
{
name: "invalid platform",
args: []string{"--platform", "<invalid>", "arg1"},
expectedError: `invalid platform`,
},
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc}) cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc})
cli.Out().SetIsTerminal(tc.isTerminal) cli.Out().SetIsTerminal(tc.isTerminal)
@ -75,13 +70,13 @@ func TestNewSaveCommandSuccess(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
isTerminal bool isTerminal bool
imageSaveFunc func(images []string, options image.SaveOptions) (io.ReadCloser, error) imageSaveFunc func(images []string) (io.ReadCloser, error)
deferredFunc func() deferredFunc func()
}{ }{
{ {
args: []string{"-o", "save_tmp_file", "arg1"}, args: []string{"-o", "save_tmp_file", "arg1"},
isTerminal: true, isTerminal: true,
imageSaveFunc: func(images []string, _ image.SaveOptions) (io.ReadCloser, error) { imageSaveFunc: func(images []string) (io.ReadCloser, error) {
assert.Assert(t, is.Len(images, 1)) assert.Assert(t, is.Len(images, 1))
assert.Check(t, is.Equal("arg1", images[0])) assert.Check(t, is.Equal("arg1", images[0]))
return io.NopCloser(strings.NewReader("")), nil return io.NopCloser(strings.NewReader("")), nil
@ -93,28 +88,21 @@ func TestNewSaveCommandSuccess(t *testing.T) {
{ {
args: []string{"arg1", "arg2"}, args: []string{"arg1", "arg2"},
isTerminal: false, isTerminal: false,
imageSaveFunc: func(images []string, _ image.SaveOptions) (io.ReadCloser, error) { imageSaveFunc: func(images []string) (io.ReadCloser, error) {
assert.Assert(t, is.Len(images, 2)) assert.Assert(t, is.Len(images, 2))
assert.Check(t, is.Equal("arg1", images[0])) assert.Check(t, is.Equal("arg1", images[0]))
assert.Check(t, is.Equal("arg2", images[1])) assert.Check(t, is.Equal("arg2", images[1]))
return io.NopCloser(strings.NewReader("")), nil return io.NopCloser(strings.NewReader("")), nil
}, },
}, },
{
args: []string{"--platform", "linux/amd64", "arg1"},
isTerminal: false,
imageSaveFunc: func(images []string, options image.SaveOptions) (io.ReadCloser, error) {
assert.Assert(t, is.Len(images, 1))
assert.Check(t, is.Equal("arg1", images[0]))
assert.Check(t, is.DeepEqual(ocispec.Platform{OS: "linux", Architecture: "amd64"}, *options.Platform))
return io.NopCloser(strings.NewReader("")), nil
},
},
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(strings.Join(tc.args, " "), func(t *testing.T) { t.Run(strings.Join(tc.args, " "), func(t *testing.T) {
cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{ cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{
imageSaveFunc: tc.imageSaveFunc, imageSaveFunc: func(images []string, options image.SaveOptions) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("")), nil
},
})) }))
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard) cmd.SetErr(io.Discard)

View File

@ -1,2 +0,0 @@
IMAGE CREATED CREATED BY SIZE COMMENT
123456789012 Less than a second ago 0B

View File

@ -56,6 +56,7 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error
continue continue
} }
im := im
sub := subImage{ sub := subImage{
Platform: platforms.Format(im.ImageData.Platform), Platform: platforms.Format(im.ImageData.Platform),
Available: im.Available, Available: im.Available,

View File

@ -31,6 +31,7 @@ func TestManifestCreateErrors(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.expectedError, func(t *testing.T) { t.Run(tc.expectedError, func(t *testing.T) {
cli := test.NewFakeCli(nil) cli := test.NewFakeCli(nil)
cmd := newCreateListCommand(cli) cmd := newCreateListCommand(cli)

View File

@ -218,6 +218,7 @@ func TestNetworkCreateIPv6(t *testing.T) {
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
networkCreateFunc: func(ctx context.Context, name string, createBody network.CreateOptions) (network.CreateResponse, error) { networkCreateFunc: func(ctx context.Context, name string, createBody network.CreateOptions) (network.CreateResponse, error) {

View File

@ -161,6 +161,7 @@ foobar_bar 2017-01-01 00:00:00 +0000 UTC
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out

View File

@ -83,6 +83,7 @@ func TestNetworkList(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{networkListFunc: tc.networkListFunc}) cli := test.NewFakeCli(&fakeClient{networkListFunc: tc.networkListFunc})
cmd := newListCommand(cli) cmd := newListCommand(cli)

View File

@ -63,6 +63,7 @@ func TestNetworkRemoveForce(t *testing.T) {
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
fakeCli := test.NewFakeCli(&fakeClient{ fakeCli := test.NewFakeCli(&fakeClient{
networkRemoveFunc: func(ctx context.Context, networkID string) error { networkRemoveFunc: func(ctx context.Context, networkID string) error {

View File

@ -202,6 +202,7 @@ foobar_boo Unknown
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out

View File

@ -106,6 +106,7 @@ func TestNodeInspectPretty(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
nodeInspectFunc: tc.nodeInspectFunc, nodeInspectFunc: tc.nodeInspectFunc,

View File

@ -134,6 +134,7 @@ func TestNodePs(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
infoFunc: tc.infoFunc, infoFunc: tc.infoFunc,

View File

@ -131,6 +131,7 @@ foobar_bar
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out

View File

@ -66,6 +66,7 @@ func TestInspectErrors(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.description, func(t *testing.T) { t.Run(tc.description, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{pluginInspectFunc: tc.inspectFunc}) cli := test.NewFakeCli(&fakeClient{pluginInspectFunc: tc.inspectFunc})
cmd := newInspectCommand(cli) cmd := newInspectCommand(cli)
@ -137,6 +138,7 @@ func TestInspect(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.description, func(t *testing.T) { t.Run(tc.description, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{pluginInspectFunc: tc.inspectFunc}) cli := test.NewFakeCli(&fakeClient{pluginInspectFunc: tc.inspectFunc})
cmd := newInspectCommand(cli) cmd := newInspectCommand(cli)

View File

@ -54,6 +54,7 @@ func TestInstallErrors(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.description, func(t *testing.T) { t.Run(tc.description, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc}) cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc})
cmd := newInstallCommand(cli) cmd := newInstallCommand(cli)
@ -93,6 +94,7 @@ func TestInstallContentTrustErrors(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.description, func(t *testing.T) { t.Run(tc.description, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
pluginInstallFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) { pluginInstallFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
@ -136,6 +138,7 @@ func TestInstall(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.description, func(t *testing.T) { t.Run(tc.description, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc}) cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc})
cmd := newInstallCommand(cli) cmd := newInstallCommand(cli)

View File

@ -46,6 +46,7 @@ func TestListErrors(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.description, func(t *testing.T) { t.Run(tc.description, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc}) cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc})
cmd := newListCommand(cli) cmd := newListCommand(cli)
@ -165,6 +166,7 @@ func TestList(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.description, func(t *testing.T) { t.Run(tc.description, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc}) cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc})
cmd := newListCommand(cli) cmd := newListCommand(cli)

View File

@ -2,7 +2,6 @@ package plugin
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
@ -37,13 +36,17 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
} }
func runRemove(ctx context.Context, dockerCli command.Cli, opts *rmOptions) error { func runRemove(ctx context.Context, dockerCli command.Cli, opts *rmOptions) error {
var errs error var errs cli.Errors
for _, name := range opts.plugins { for _, name := range opts.plugins {
if err := dockerCli.Client().PluginRemove(ctx, name, types.PluginRemoveOptions{Force: opts.force}); err != nil { if err := dockerCli.Client().PluginRemove(ctx, name, types.PluginRemoveOptions{Force: opts.force}); err != nil {
errs = errors.Join(errs, err) errs = append(errs, err)
continue continue
} }
_, _ = fmt.Fprintln(dockerCli.Out(), name) fmt.Fprintln(dockerCli.Out(), name)
} }
return errs // Do not simplify to `return errs` because even if errs == nil, it is not a nil-error interface value.
if errs != nil {
return errs
}
return nil
} }

View File

@ -18,24 +18,20 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
const ( const patSuggest = "You can log in with your password or a Personal Access " +
registerSuggest = "Log in with your Docker ID or email address to push and pull images from Docker Hub. " + "Token (PAT). Using a limited-scope PAT grants better security and is required " +
"If you don't have a Docker ID, head over to https://hub.docker.com/ to create one." "for organizations using SSO. Learn more at https://docs.docker.com/go/access-tokens/"
patSuggest = "You can log in with your password or a Personal Access " +
"Token (PAT). Using a limited-scope PAT grants better security and is required " +
"for organizations using SSO. Learn more at https://docs.docker.com/go/access-tokens/"
)
// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info // RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
// for the given command. // for the given command.
func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) registrytypes.RequestAuthConfig { func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) registrytypes.RequestAuthConfig {
return func(ctx context.Context) (string, error) { return func(ctx context.Context) (string, error) {
_, _ = fmt.Fprintf(cli.Out(), "\nLogin prior to %s:\n", cmdName) fmt.Fprintf(cli.Out(), "\nLogin prior to %s:\n", cmdName)
indexServer := registry.GetAuthConfigKey(index) indexServer := registry.GetAuthConfigKey(index)
isDefaultRegistry := indexServer == registry.IndexServer isDefaultRegistry := indexServer == registry.IndexServer
authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, indexServer, isDefaultRegistry) authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, indexServer, isDefaultRegistry)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", indexServer, err) fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", indexServer, err)
} }
select { select {
@ -90,8 +86,7 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
} }
// ConfigureAuth handles prompting of user's username and password if needed. // ConfigureAuth handles prompting of user's username and password if needed.
// // Deprecated: use PromptUserForCredentials instead.
// Deprecated: use [PromptUserForCredentials] instead.
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authConfig *registrytypes.AuthConfig, _ bool) error { func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authConfig *registrytypes.AuthConfig, _ bool) error {
defaultUsername := authConfig.Username defaultUsername := authConfig.Username
serverAddress := authConfig.ServerAddress serverAddress := authConfig.ServerAddress
@ -115,7 +110,7 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
// If defaultUsername is not empty, the username prompt includes that username // If defaultUsername is not empty, the username prompt includes that username
// and the user can hit enter without inputting a username to use that default // and the user can hit enter without inputting a username to use that default
// username. // username.
func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (registrytypes.AuthConfig, error) { func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (authConfig registrytypes.AuthConfig, err error) {
// On Windows, force the use of the regular OS stdin stream. // On Windows, force the use of the regular OS stdin stream.
// //
// See: // See:
@ -128,71 +123,57 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
cli.SetIn(streams.NewIn(os.Stdin)) cli.SetIn(streams.NewIn(os.Stdin))
} }
argUser = strings.TrimSpace(argUser) isDefaultRegistry := serverAddress == registry.IndexServer
if argUser == "" { defaultUsername = strings.TrimSpace(defaultUsername)
if serverAddress == registry.IndexServer {
// When signing in to the default (Docker Hub) registry, we display if argUser = strings.TrimSpace(argUser); argUser == "" {
// hints for creating an account, and (if hints are enabled), using if isDefaultRegistry {
// a token instead of a password. // if this is a default registry (docker hub), then display the following message.
_, _ = fmt.Fprintln(cli.Out(), registerSuggest) fmt.Fprintln(cli.Out(), "Log in with your Docker ID or email address to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com/ to create one.")
if hints.Enabled() { if hints.Enabled() {
_, _ = fmt.Fprintln(cli.Out(), patSuggest) fmt.Fprintln(cli.Out(), patSuggest)
_, _ = fmt.Fprintln(cli.Out()) fmt.Fprintln(cli.Out())
} }
} }
var prompt string var prompt string
defaultUsername = strings.TrimSpace(defaultUsername)
if defaultUsername == "" { if defaultUsername == "" {
prompt = "Username: " prompt = "Username: "
} else { } else {
prompt = fmt.Sprintf("Username (%s): ", defaultUsername) prompt = fmt.Sprintf("Username (%s): ", defaultUsername)
} }
var err error
argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt) argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
if err != nil { if err != nil {
return registrytypes.AuthConfig{}, err return authConfig, err
} }
if argUser == "" { if argUser == "" {
argUser = defaultUsername argUser = defaultUsername
} }
if argUser == "" {
return registrytypes.AuthConfig{}, errors.Errorf("Error: Non-null Username Required")
}
} }
if argUser == "" {
argPassword = strings.TrimSpace(argPassword) return authConfig, errors.Errorf("Error: Non-null Username Required")
}
if argPassword == "" { if argPassword == "" {
restoreInput, err := DisableInputEcho(cli.In()) restoreInput, err := DisableInputEcho(cli.In())
if err != nil { if err != nil {
return registrytypes.AuthConfig{}, err return authConfig, err
} }
defer func() { defer restoreInput()
if err := restoreInput(); err != nil {
// TODO(thaJeztah): we should consider printing instructions how
// to restore this manually (other than restarting the shell).
// e.g., 'run stty echo' when in a Linux or macOS shell, but
// PowerShell and CMD.exe may need different instructions.
_, _ = fmt.Fprintln(cli.Err(), "Error: failed to restore terminal state to echo input:", err)
}
}()
argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ") argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
if err != nil { if err != nil {
return registrytypes.AuthConfig{}, err return authConfig, err
} }
_, _ = fmt.Fprintln(cli.Out()) fmt.Fprint(cli.Out(), "\n")
if argPassword == "" { if argPassword == "" {
return registrytypes.AuthConfig{}, errors.Errorf("Error: Password Required") return authConfig, errors.Errorf("Error: Password Required")
} }
} }
return registrytypes.AuthConfig{ authConfig.Username = argUser
Username: argUser, authConfig.Password = argPassword
Password: argPassword, authConfig.ServerAddress = serverAddress
ServerAddress: serverAddress, return authConfig, nil
}, nil
} }
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete // RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete

View File

@ -203,6 +203,7 @@ result2 5
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
err := SearchWrite(formatter.Context{Format: tc.format, Output: &out}, results) err := SearchWrite(formatter.Context{Format: tc.format, Output: &out}, results)

View File

@ -58,9 +58,9 @@ func NewLoginCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func verifyLoginOptions(dockerCli command.Cli, opts *loginOptions) error { func verifyloginOptions(dockerCli command.Cli, opts *loginOptions) error {
if opts.password != "" { if opts.password != "" {
_, _ = fmt.Fprintln(dockerCli.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.") fmt.Fprintln(dockerCli.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
if opts.passwordStdin { if opts.passwordStdin {
return errors.New("--password and --password-stdin are mutually exclusive") return errors.New("--password and --password-stdin are mutually exclusive")
} }
@ -83,7 +83,7 @@ func verifyLoginOptions(dockerCli command.Cli, opts *loginOptions) error {
} }
func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error { func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error {
if err := verifyLoginOptions(dockerCli, &opts); err != nil { if err := verifyloginOptions(dockerCli, &opts); err != nil {
return err return err
} }
var ( var (
@ -174,7 +174,7 @@ func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, de
if !errors.Is(err, manager.ErrDeviceLoginStartFail) { if !errors.Is(err, manager.ErrDeviceLoginStartFail) {
return response, err return response, err
} }
_, _ = fmt.Fprint(dockerCli.Err(), "Failed to start web-based login - falling back to command line login...\n\n") fmt.Fprint(dockerCli.Err(), "Failed to start web-based login - falling back to command line login...\n\n")
} }
return loginWithUsernameAndPassword(ctx, dockerCli, opts, defaultUsername, serverAddress) return loginWithUsernameAndPassword(ctx, dockerCli, opts, defaultUsername, serverAddress)

View File

@ -61,6 +61,7 @@ id_rsa
}, },
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out

View File

@ -93,6 +93,7 @@ func TestSecretInspectWithoutFormat(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
secretInspectFunc: tc.secretInspectFunc, secretInspectFunc: tc.secretInspectFunc,
@ -131,6 +132,7 @@ func TestSecretInspectWithFormat(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
secretInspectFunc: tc.secretInspectFunc, secretInspectFunc: tc.secretInspectFunc,

View File

@ -223,6 +223,7 @@ zarp2
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out

View File

@ -168,6 +168,7 @@ func TestServiceListServiceStatus(t *testing.T) {
} }
for _, tc := range matrix { for _, tc := range matrix {
tc := tc
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
if tc.cluster == nil { if tc.cluster == nil {
tc.cluster = generateCluster(t, tc.opts) tc.cluster = generateCluster(t, tc.opts)

View File

@ -50,6 +50,7 @@ func TestCredentialSpecOpt(t *testing.T) {
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
var cs credentialSpecOpt var cs credentialSpecOpt

View File

@ -91,6 +91,7 @@ func TestRollbackWithErrors(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := newRollbackCommand( cmd := newRollbackCommand(
test.NewFakeCli(&fakeClient{ test.NewFakeCli(&fakeClient{

View File

@ -1058,6 +1058,7 @@ func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
// Build the current list of portConfig // Build the current list of portConfig
for _, entry := range *portConfig { for _, entry := range *portConfig {
entry := entry
if _, ok := portSet[portConfigToString(&entry)]; !ok { if _, ok := portSet[portConfigToString(&entry)]; !ok {
portSet[portConfigToString(&entry)] = entry portSet[portConfigToString(&entry)] = entry
} }
@ -1085,6 +1086,7 @@ portLoop:
ports := flags.Lookup(flagPublishAdd).Value.(*opts.PortOpt).Value() ports := flags.Lookup(flagPublishAdd).Value.(*opts.PortOpt).Value()
for _, port := range ports { for _, port := range ports {
port := port
if _, ok := portSet[portConfigToString(&port)]; ok { if _, ok := portSet[portConfigToString(&port)]; ok {
continue continue
} }

View File

@ -1690,6 +1690,7 @@ func TestUpdateUlimits(t *testing.T) {
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
svc := swarm.ServiceSpec{ svc := swarm.ServiceSpec{
TaskTemplate: swarm.TaskSpec{ TaskTemplate: swarm.TaskSpec{

View File

@ -51,6 +51,7 @@ bar
{Name: "bar", Services: 1}, {Name: "bar", Services: 1},
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out

View File

@ -48,6 +48,7 @@ func TestListErrors(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.expectedError, func(t *testing.T) { t.Run(tc.expectedError, func(t *testing.T) {
cmd := newListCommand(test.NewFakeCli(&fakeClient{ cmd := newListCommand(test.NewFakeCli(&fakeClient{
serviceListFunc: tc.serviceListFunc, serviceListFunc: tc.serviceListFunc,
@ -103,6 +104,7 @@ func TestStackList(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
var services []swarm.Service var services []swarm.Service
for _, name := range tc.serviceNames { for _, name := range tc.serviceNames {

View File

@ -40,6 +40,7 @@ func TestStackPsErrors(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.expectedError, func(t *testing.T) { t.Run(tc.expectedError, func(t *testing.T) {
cmd := newPsCommand(test.NewFakeCli(&fakeClient{ cmd := newPsCommand(test.NewFakeCli(&fakeClient{
taskListFunc: tc.taskListFunc, taskListFunc: tc.taskListFunc,
@ -159,6 +160,7 @@ func TestStackPs(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
taskListFunc: tc.taskListFunc, taskListFunc: tc.taskListFunc,

View File

@ -67,6 +67,7 @@ func TestStackServicesErrors(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.expectedError, func(t *testing.T) { t.Run(tc.expectedError, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
serviceListFunc: tc.serviceListFunc, serviceListFunc: tc.serviceListFunc,

View File

@ -88,6 +88,7 @@ func TestServiceUpdateResolveImageChanged(t *testing.T) {
ctx := context.Background() ctx := context.Background()
for _, tc := range testcases { for _, tc := range testcases {
tc := tc
t.Run(tc.image, func(t *testing.T) { t.Run(tc.image, func(t *testing.T) {
spec := map[string]swarm.ServiceSpec{ spec := map[string]swarm.ServiceSpec{
"myservice": { "myservice": {

View File

@ -63,6 +63,7 @@ func TestSwarmInitErrorOnAPIFailure(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := newInitCommand( cmd := newInitCommand(
test.NewFakeCli(&fakeClient{ test.NewFakeCli(&fakeClient{

View File

@ -48,6 +48,7 @@ func TestSwarmJoinErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := newJoinCommand( cmd := newJoinCommand(
test.NewFakeCli(&fakeClient{ test.NewFakeCli(&fakeClient{
@ -92,6 +93,7 @@ func TestSwarmJoin(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
infoFunc: tc.infoFunc, infoFunc: tc.infoFunc,

View File

@ -87,6 +87,7 @@ func TestSwarmJoinTokenErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
swarmInspectFunc: tc.swarmInspectFunc, swarmInspectFunc: tc.swarmInspectFunc,
@ -197,6 +198,7 @@ func TestSwarmJoinToken(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
swarmInspectFunc: tc.swarmInspectFunc, swarmInspectFunc: tc.swarmInspectFunc,

View File

@ -32,6 +32,7 @@ func TestSwarmLeaveErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := newLeaveCommand( cmd := newLeaveCommand(
test.NewFakeCli(&fakeClient{ test.NewFakeCli(&fakeClient{

View File

@ -80,6 +80,7 @@ func TestSwarmUnlockKeyErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := newUnlockKeyCommand( cmd := newUnlockKeyCommand(
test.NewFakeCli(&fakeClient{ test.NewFakeCli(&fakeClient{
@ -157,6 +158,7 @@ func TestSwarmUnlockKey(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
swarmInspectFunc: tc.swarmInspectFunc, swarmInspectFunc: tc.swarmInspectFunc,

View File

@ -64,6 +64,7 @@ func TestSwarmUnlockErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := newUnlockCommand( cmd := newUnlockCommand(
test.NewFakeCli(&fakeClient{ test.NewFakeCli(&fakeClient{

View File

@ -65,6 +65,7 @@ func TestSwarmUpdateErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := newUpdateCommand( cmd := newUpdateCommand(
test.NewFakeCli(&fakeClient{ test.NewFakeCli(&fakeClient{
@ -168,6 +169,7 @@ func TestSwarmUpdate(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
swarmInspectFunc: tc.swarmInspectFunc, swarmInspectFunc: tc.swarmInspectFunc,

View File

@ -7,11 +7,7 @@ import (
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client" "github.com/docker/docker/client"
) )
@ -19,27 +15,22 @@ type fakeClient struct {
client.Client client.Client
version string version string
containerListFunc func(context.Context, container.ListOptions) ([]container.Summary, error)
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error)
eventsFn func(context.Context, events.ListOptions) (<-chan events.Message, <-chan error)
imageListFunc func(ctx context.Context, options image.ListOptions) ([]image.Summary, error)
infoFunc func(ctx context.Context) (system.Info, error)
networkListFunc func(ctx context.Context, options network.ListOptions) ([]network.Summary, error)
networkPruneFunc func(ctx context.Context, pruneFilter filters.Args) (network.PruneReport, error)
nodeListFunc func(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error)
serverVersion func(ctx context.Context) (types.Version, error) serverVersion func(ctx context.Context) (types.Version, error)
volumeListFunc func(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) eventsFn func(context.Context, events.ListOptions) (<-chan events.Message, <-chan error)
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error)
networkPruneFunc func(ctx context.Context, pruneFilter filters.Args) (network.PruneReport, error)
}
func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) {
return cli.serverVersion(ctx)
} }
func (cli *fakeClient) ClientVersion() string { func (cli *fakeClient) ClientVersion() string {
return cli.version return cli.version
} }
func (cli *fakeClient) ContainerList(ctx context.Context, options container.ListOptions) ([]container.Summary, error) { func (cli *fakeClient) Events(ctx context.Context, opts events.ListOptions) (<-chan events.Message, <-chan error) {
if cli.containerListFunc != nil { return cli.eventsFn(ctx, opts)
return cli.containerListFunc(ctx, options)
}
return []container.Summary{}, nil
} }
func (cli *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) { func (cli *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
@ -49,52 +40,9 @@ func (cli *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters
return container.PruneReport{}, nil return container.PruneReport{}, nil
} }
func (cli *fakeClient) Events(ctx context.Context, opts events.ListOptions) (<-chan events.Message, <-chan error) {
return cli.eventsFn(ctx, opts)
}
func (cli *fakeClient) ImageList(ctx context.Context, options image.ListOptions) ([]image.Summary, error) {
if cli.imageListFunc != nil {
return cli.imageListFunc(ctx, options)
}
return []image.Summary{}, nil
}
func (cli *fakeClient) Info(ctx context.Context) (system.Info, error) {
if cli.infoFunc != nil {
return cli.infoFunc(ctx)
}
return system.Info{}, nil
}
func (cli *fakeClient) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
if cli.networkListFunc != nil {
return cli.networkListFunc(ctx, options)
}
return []network.Summary{}, nil
}
func (cli *fakeClient) NetworksPrune(ctx context.Context, pruneFilter filters.Args) (network.PruneReport, error) { func (cli *fakeClient) NetworksPrune(ctx context.Context, pruneFilter filters.Args) (network.PruneReport, error) {
if cli.networkPruneFunc != nil { if cli.networkPruneFunc != nil {
return cli.networkPruneFunc(ctx, pruneFilter) return cli.networkPruneFunc(ctx, pruneFilter)
} }
return network.PruneReport{}, nil return network.PruneReport{}, nil
} }
func (cli *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
if cli.nodeListFunc != nil {
return cli.nodeListFunc(ctx, options)
}
return []swarm.Node{}, nil
}
func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) {
return cli.serverVersion(ctx)
}
func (cli *fakeClient) VolumeList(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) {
if cli.volumeListFunc != nil {
return cli.volumeListFunc(ctx, options)
}
return volume.ListResponse{}, nil
}

View File

@ -1,237 +0,0 @@
package system
import (
"strings"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/spf13/cobra"
)
var (
eventFilters = []string{"container", "daemon", "event", "image", "label", "network", "node", "scope", "type", "volume"}
// eventTypes is a list of all event types.
// This should be moved to the moby codebase once its usage is consolidated here.
eventTypes = []events.Type{
events.BuilderEventType,
events.ConfigEventType,
events.ContainerEventType,
events.DaemonEventType,
events.ImageEventType,
events.NetworkEventType,
events.NodeEventType,
events.PluginEventType,
events.SecretEventType,
events.ServiceEventType,
events.VolumeEventType,
}
// eventActions is a list of all event actions.
// This should be moved to the moby codebase once its usage is consolidated here.
eventActions = []events.Action{
events.ActionCreate,
events.ActionStart,
events.ActionRestart,
events.ActionStop,
events.ActionCheckpoint,
events.ActionPause,
events.ActionUnPause,
events.ActionAttach,
events.ActionDetach,
events.ActionResize,
events.ActionUpdate,
events.ActionRename,
events.ActionKill,
events.ActionDie,
events.ActionOOM,
events.ActionDestroy,
events.ActionRemove,
events.ActionCommit,
events.ActionTop,
events.ActionCopy,
events.ActionArchivePath,
events.ActionExtractToDir,
events.ActionExport,
events.ActionImport,
events.ActionSave,
events.ActionLoad,
events.ActionTag,
events.ActionUnTag,
events.ActionPush,
events.ActionPull,
events.ActionPrune,
events.ActionDelete,
events.ActionEnable,
events.ActionDisable,
events.ActionConnect,
events.ActionDisconnect,
events.ActionReload,
events.ActionMount,
events.ActionUnmount,
events.ActionExecCreate,
events.ActionExecStart,
events.ActionExecDie,
events.ActionExecDetach,
events.ActionHealthStatus,
events.ActionHealthStatusRunning,
events.ActionHealthStatusHealthy,
events.ActionHealthStatusUnhealthy,
}
)
// completeEventFilters provides completion for the filters that can be used with `--filter`.
func completeEventFilters(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
key, _, ok := strings.Cut(toComplete, "=")
if !ok {
return postfixWith("=", eventFilters), cobra.ShellCompDirectiveNoSpace
}
switch key {
case "container":
return prefixWith("container=", containerNames(dockerCLI, cmd, args, toComplete)), cobra.ShellCompDirectiveNoFileComp
case "daemon":
return prefixWith("daemon=", daemonNames(dockerCLI, cmd)), cobra.ShellCompDirectiveNoFileComp
case "event":
return prefixWith("event=", validEventNames()), cobra.ShellCompDirectiveNoFileComp
case "image":
return prefixWith("image=", imageNames(dockerCLI, cmd)), cobra.ShellCompDirectiveNoFileComp
case "label":
return nil, cobra.ShellCompDirectiveNoFileComp
case "network":
return prefixWith("network=", networkNames(dockerCLI, cmd)), cobra.ShellCompDirectiveNoFileComp
case "node":
return prefixWith("node=", nodeNames(dockerCLI, cmd)), cobra.ShellCompDirectiveNoFileComp
case "scope":
return prefixWith("scope=", []string{"local", "swarm"}), cobra.ShellCompDirectiveNoFileComp
case "type":
return prefixWith("type=", eventTypeNames()), cobra.ShellCompDirectiveNoFileComp
case "volume":
return prefixWith("volume=", volumeNames(dockerCLI, cmd)), cobra.ShellCompDirectiveNoFileComp
default:
return postfixWith("=", eventFilters), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
}
}
}
// prefixWith prefixes every element in the slice with the given prefix.
func prefixWith(prefix string, values []string) []string {
result := make([]string, len(values))
for i, v := range values {
result[i] = prefix + v
}
return result
}
// postfixWith appends postfix to every element in the slice.
func postfixWith(postfix string, values []string) []string {
result := make([]string, len(values))
for i, v := range values {
result[i] = v + postfix
}
return result
}
// eventTypeNames provides a list of all event types.
// The list is derived from eventTypes.
func eventTypeNames() []string {
names := make([]string, len(eventTypes))
for i, eventType := range eventTypes {
names[i] = string(eventType)
}
return names
}
// validEventNames provides a list of all event actions.
// The list is derived from eventActions.
// Actions that are not suitable for usage in completions are removed.
func validEventNames() []string {
names := make([]string, 0, len(eventActions))
for _, eventAction := range eventActions {
if strings.Contains(string(eventAction), " ") {
continue
}
names = append(names, string(eventAction))
}
return names
}
// containerNames contacts the API to get names and optionally IDs of containers.
// In case of an error, an empty list is returned.
func containerNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command, args []string, toComplete string) []string {
names, _ := completion.ContainerNames(dockerCLI, true)(cmd, args, toComplete)
if names == nil {
return []string{}
}
return names
}
// daemonNames contacts the API to get name and ID of the current docker daemon.
// In case of an error, an empty list is returned.
func daemonNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string {
info, err := dockerCLI.Client().Info(cmd.Context())
if err != nil {
return []string{}
}
return []string{info.Name, info.ID}
}
// imageNames contacts the API to get a list of image names.
// In case of an error, an empty list is returned.
func imageNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string {
list, err := dockerCLI.Client().ImageList(cmd.Context(), image.ListOptions{})
if err != nil {
return []string{}
}
names := make([]string, 0, len(list))
for _, img := range list {
names = append(names, img.RepoTags...)
}
return names
}
// networkNames contacts the API to get a list of network names.
// In case of an error, an empty list is returned.
func networkNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string {
list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{})
if err != nil {
return []string{}
}
names := make([]string, 0, len(list))
for _, nw := range list {
names = append(names, nw.Name)
}
return names
}
// nodeNames contacts the API to get a list of node names.
// In case of an error, an empty list is returned.
func nodeNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string {
list, err := dockerCLI.Client().NodeList(cmd.Context(), types.NodeListOptions{})
if err != nil {
return []string{}
}
names := make([]string, 0, len(list))
for _, node := range list {
names = append(names, node.Description.Hostname)
}
return names
}
// volumeNames contacts the API to get a list of volume names.
// In case of an error, an empty list is returned.
func volumeNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string {
list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{})
if err != nil {
return []string{}
}
names := make([]string, 0, len(list.Volumes))
for _, v := range list.Volumes {
names = append(names, v.Name)
}
return names
}

View File

@ -1,165 +0,0 @@
package system
import (
"context"
"errors"
"fmt"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/builders"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/api/types/volume"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
)
func TestCompleteEventFilter(t *testing.T) {
tests := []struct {
client *fakeClient
toComplete string
expected []string
}{
{
client: &fakeClient{
containerListFunc: func(_ context.Context, _ container.ListOptions) ([]container.Summary, error) {
return []container.Summary{
*builders.Container("c1"),
*builders.Container("c2"),
}, nil
},
},
toComplete: "container=",
expected: []string{"container=c1", "container=c2"},
},
{
client: &fakeClient{
containerListFunc: func(_ context.Context, _ container.ListOptions) ([]container.Summary, error) {
return nil, errors.New("API error")
},
},
toComplete: "container=",
expected: []string{},
},
{
client: &fakeClient{
infoFunc: func(ctx context.Context) (system.Info, error) {
return system.Info{
ID: "daemon-id",
Name: "daemon-name",
}, nil
},
},
toComplete: "daemon=",
expected: []string{"daemon=daemon-name", "daemon=daemon-id"},
},
{
client: &fakeClient{
infoFunc: func(ctx context.Context) (system.Info, error) {
return system.Info{}, errors.New("API error")
},
},
toComplete: "daemon=",
expected: []string{},
},
{
client: &fakeClient{
imageListFunc: func(_ context.Context, _ image.ListOptions) ([]image.Summary, error) {
return []image.Summary{
{RepoTags: []string{"img:1"}},
{RepoTags: []string{"img:2"}},
}, nil
},
},
toComplete: "image=",
expected: []string{"image=img:1", "image=img:2"},
},
{
client: &fakeClient{
imageListFunc: func(_ context.Context, _ image.ListOptions) ([]image.Summary, error) {
return []image.Summary{}, errors.New("API error")
},
},
toComplete: "image=",
expected: []string{},
},
{
client: &fakeClient{
networkListFunc: func(_ context.Context, _ network.ListOptions) ([]network.Summary, error) {
return []network.Summary{
*builders.NetworkResource(builders.NetworkResourceName("nw1")),
*builders.NetworkResource(builders.NetworkResourceName("nw2")),
}, nil
},
},
toComplete: "network=",
expected: []string{"network=nw1", "network=nw2"},
},
{
client: &fakeClient{
networkListFunc: func(_ context.Context, _ network.ListOptions) ([]network.Summary, error) {
return nil, errors.New("API error")
},
},
toComplete: "network=",
expected: []string{},
},
{
client: &fakeClient{
nodeListFunc: func(_ context.Context, _ types.NodeListOptions) ([]swarm.Node, error) {
return []swarm.Node{
*builders.Node(builders.Hostname("n1")),
}, nil
},
},
toComplete: "node=",
expected: []string{"node=n1"},
},
{
client: &fakeClient{
nodeListFunc: func(_ context.Context, _ types.NodeListOptions) ([]swarm.Node, error) {
return []swarm.Node{}, errors.New("API error")
},
},
toComplete: "node=",
expected: []string{},
},
{
client: &fakeClient{
volumeListFunc: func(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) {
return volume.ListResponse{
Volumes: []*volume.Volume{
builders.Volume(builders.VolumeName("v1")),
builders.Volume(builders.VolumeName("v2")),
},
}, nil
},
},
toComplete: "volume=",
expected: []string{"volume=v1", "volume=v2"},
},
{
client: &fakeClient{
volumeListFunc: func(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) {
return volume.ListResponse{}, errors.New("API error")
},
},
toComplete: "volume=",
expected: []string{},
},
}
for _, tc := range tests {
cli := test.NewFakeCli(tc.client)
completions, directive := completeEventFilters(cli)(NewEventsCommand(cli), nil, tc.toComplete)
assert.DeepEqual(t, completions, tc.expected)
assert.Equal(t, directive, cobra.ShellCompDirectiveNoFileComp, fmt.Sprintf("wrong directive in completion for '%s'", tc.toComplete))
}
}

View File

@ -50,8 +50,6 @@ func NewEventsCommand(dockerCli command.Cli) *cobra.Command {
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided") flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
flags.StringVar(&options.format, "format", "", flagsHelper.InspectFormatHelp) // using the same flag description as "inspect" commands for now. flags.StringVar(&options.format, "format", "", flagsHelper.InspectFormatHelp) // using the same flag description as "inspect" commands for now.
_ = cmd.RegisterFlagCompletionFunc("filter", completeEventFilters(dockerCli))
return cmd return cmd
} }

View File

@ -53,6 +53,7 @@ func TestEventsFormat(t *testing.T) {
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
// Set to UTC timezone as timestamps in output are // Set to UTC timezone as timestamps in output are
// printed in the current timezone // printed in the current timezone

View File

@ -374,6 +374,7 @@ func TestPrettyPrintInfo(t *testing.T) {
expectedError: "errors pretty printing info", expectedError: "errors pretty printing info",
}, },
} { } {
tc := tc
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{}) cli := test.NewFakeCli(&fakeClient{})
err := prettyPrintInfo(cli, tc.dockerInfo) err := prettyPrintInfo(cli, tc.dockerInfo)
@ -451,6 +452,7 @@ func TestFormatInfo(t *testing.T) {
expectedError: `template: :1:2: executing "" at <.badString>: can't evaluate field badString in type system.dockerInfo`, expectedError: `template: :1:2: executing "" at <.badString>: can't evaluate field badString in type system.dockerInfo`,
}, },
} { } {
tc := tc
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{}) cli := test.NewFakeCli(&fakeClient{})
info := dockerInfo{ info := dockerInfo{
@ -516,6 +518,7 @@ func TestNeedsServerInfo(t *testing.T) {
inf := dockerInfo{ClientInfo: &clientInfo{}} inf := dockerInfo{ClientInfo: &clientInfo{}}
for _, tc := range tests { for _, tc := range tests {
tc := tc
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
assert.Equal(t, needsServerInfo(tc.template, inf), tc.expected) assert.Equal(t, needsServerInfo(tc.template, inf), tc.expected)
}) })

View File

@ -51,7 +51,7 @@ func NewInspectCommand(dockerCli command.Cli) *cobra.Command {
func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error { func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error {
var elementSearcher inspect.GetRefFunc var elementSearcher inspect.GetRefFunc
switch opts.inspectType { switch opts.inspectType {
case "", "config", "container", "image", "network", "node", "plugin", "secret", "service", "task", "volume": case "", "container", "image", "node", "network", "service", "volume", "task", "plugin", "secret":
elementSearcher = inspectAll(ctx, dockerCli, opts.size, opts.inspectType) elementSearcher = inspectAll(ctx, dockerCli, opts.size, opts.inspectType)
default: default:
return errors.Errorf("%q is not a valid value for --type", opts.inspectType) return errors.Errorf("%q is not a valid value for --type", opts.inspectType)
@ -114,12 +114,6 @@ func inspectSecret(ctx context.Context, dockerCli command.Cli) inspect.GetRefFun
} }
} }
func inspectConfig(ctx context.Context, dockerCLI command.Cli) inspect.GetRefFunc {
return func(ref string) (any, []byte, error) {
return dockerCLI.Client().ConfigInspectWithRaw(ctx, ref)
}
}
func inspectAll(ctx context.Context, dockerCli command.Cli, getSize bool, typeConstraint string) inspect.GetRefFunc { func inspectAll(ctx context.Context, dockerCli command.Cli, getSize bool, typeConstraint string) inspect.GetRefFunc {
inspectAutodetect := []struct { inspectAutodetect := []struct {
objectType string objectType string
@ -168,11 +162,6 @@ func inspectAll(ctx context.Context, dockerCli command.Cli, getSize bool, typeCo
isSwarmObject: true, isSwarmObject: true,
objectInspector: inspectSecret(ctx, dockerCli), objectInspector: inspectSecret(ctx, dockerCli),
}, },
{
objectType: "config",
isSwarmObject: true,
objectInspector: inspectConfig(ctx, dockerCli),
},
} }
// isSwarmManager does an Info API call to verify that the daemon is // isSwarmManager does an Info API call to verify that the daemon is

View File

@ -71,6 +71,7 @@ foobar_bar foo2
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out

View File

@ -56,6 +56,7 @@ func TestGetFullCommandName(t *testing.T) {
expected: "root child grandchild", expected: "root child grandchild",
}, },
} { } {
tc := tc
t.Run(tc.testName, func(t *testing.T) { t.Run(tc.testName, func(t *testing.T) {
t.Parallel() t.Parallel()
actual := getFullCommandName(tc.cmd) actual := getFullCommandName(tc.cmd)
@ -90,6 +91,7 @@ func TestGetCommandName(t *testing.T) {
expected: "child grandchild", expected: "child grandchild",
}, },
} { } {
tc := tc
t.Run(tc.testName, func(t *testing.T) { t.Run(tc.testName, func(t *testing.T) {
t.Parallel() t.Parallel()
actual := getCommandName(tc.cmd) actual := getCommandName(tc.cmd)
@ -128,6 +130,7 @@ func TestStdioAttributes(t *testing.T) {
}, },
}, },
} { } {
tc := tc
t.Run(tc.test, func(t *testing.T) { t.Run(tc.test, func(t *testing.T) {
t.Parallel() t.Parallel()
cli := &DockerCli{ cli := &DockerCli{
@ -176,6 +179,7 @@ func TestAttributesFromError(t *testing.T) {
}, },
}, },
} { } {
tc := tc
t.Run(tc.testName, func(t *testing.T) { t.Run(tc.testName, func(t *testing.T) {
t.Parallel() t.Parallel()
actual := attributesFromError(tc.err) actual := attributesFromError(tc.err)

View File

@ -127,6 +127,7 @@ tag3 bbbbbbbb
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out
@ -230,6 +231,7 @@ eve foobarbazquxquux, key31, key32
{Name: "eve", Keys: []string{"key31", "key32", "foobarbazquxquux"}}, {Name: "eve", Keys: []string{"key31", "key32", "foobarbazquxquux"}},
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out

Some files were not shown because too many files have changed in this diff Show More