diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 88350c7e8908..27cf3784f622 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,14 @@ name: build +# Default to 'contents: read', which grants actions to read commits. +# +# If any permission is set, any permission not included in the list is +# implicitly set to "none". +# +# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -13,6 +22,7 @@ on: branches: - 'master' - '[0-9]+.[0-9]+' + - '[0-9]+.x' tags: - 'v*' pull_request: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0b6bd1802488..867224bb23df 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,10 +1,20 @@ name: codeql +# Default to 'contents: read', which grants actions to read commits. +# +# If any permission is set, any permission not included in the list is +# implicitly set to "none". +# +# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions +permissions: + contents: read + on: push: branches: - 'master' - '[0-9]+.[0-9]+' + - '[0-9]+.x' tags: - 'v*' pull_request: @@ -58,7 +68,7 @@ jobs: name: Update Go uses: actions/setup-go@v5 with: - go-version: '1.21' + go-version: "1.22.12" - name: Initialize CodeQL uses: github/codeql-action/init@v3 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a2259712d9b9..3dd85a3affe6 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,5 +1,14 @@ name: e2e +# Default to 'contents: read', which grants actions to read commits. +# +# If any permission is set, any permission not included in the list is +# implicitly set to "none". +# +# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -10,6 +19,7 @@ on: branches: - 'master' - '[0-9]+.[0-9]+' + - '[0-9]+.x' tags: - 'v*' pull_request: @@ -28,9 +38,8 @@ jobs: - alpine - debian engine-version: - - 27-rc # testing - - 26.1 # latest - - 25.0 # latest - 1 + - 27.0 # latest + - 26.1 # latest - 1 - 23.0 # mirantis lts # TODO(krissetto) 19.03 needs a look, doesn't work ubuntu 22.04 (cgroup errors). # we could have a separate job that tests it against ubuntu 20.04 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2bd51e576f4c..9635ae251285 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,14 @@ name: test +# Default to 'contents: read', which grants actions to read commits. +# +# If any permission is set, any permission not included in the list is +# implicitly set to "none". +# +# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -10,6 +19,7 @@ on: branches: - 'master' - '[0-9]+.[0-9]+' + - '[0-9]+.x' tags: - 'v*' pull_request: @@ -46,7 +56,8 @@ jobs: fail-fast: false matrix: os: - - macos-12 + - macos-13 # macOS 13 on Intel + - macos-14 # macOS 14 on arm64 (Apple Silicon M1) # - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history steps: - @@ -64,7 +75,7 @@ jobs: name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.21.11 + go-version: "1.22.12" - name: Test run: | diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index 12ab7c076ddd..0f449d0a03d6 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -1,5 +1,14 @@ name: validate-pr +# Default to 'contents: read', which grants actions to read commits. +# +# If any permission is set, any permission not included in the list is +# implicitly set to "none". +# +# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions +permissions: + contents: read + on: pull_request: types: [opened, edited, labeled, unlabeled] @@ -53,10 +62,16 @@ jobs: # Backports or PR that target a release branch directly should mention the target branch in the title, for example: # [X.Y backport] Some change that needs backporting to X.Y # [X.Y] Change directly targeting the X.Y branch - - name: Get branch from PR title + - name: Check release branch id: title_branch - run: echo "$PR_TITLE" | sed -n 's/^\[\([0-9]*\.[0-9]*\)[^]]*\].*/branch=\1/p' >> $GITHUB_OUTPUT + run: | + # get the intended major version prefix ("[27.1 backport]" -> "27.") from the PR title. + [[ "$PR_TITLE" =~ ^\[([0-9]*\.)[^]]*\] ]] && branch="${BASH_REMATCH[1]}" - - name: Check release branch - if: github.event.pull_request.base.ref != steps.title_branch.outputs.branch && !(github.event.pull_request.base.ref == 'master' && steps.title_branch.outputs.branch == '') - run: echo "::error::PR title suggests targetting the ${{ steps.title_branch.outputs.branch }} branch, but is opened against ${{ github.event.pull_request.base.ref }}" && exit 1 + # get major version prefix from the release branch ("27.x -> "27.") + [[ "$GITHUB_BASE_REF" =~ ^([0-9]*\.) ]] && target_branch="${BASH_REMATCH[1]}" || target_branch="$GITHUB_BASE_REF" + + if [[ "$target_branch" != "$branch" ]] && ! [[ "$GITHUB_BASE_REF" == "master" && "$branch" == "" ]]; then + echo "::error::PR is opened against the $GITHUB_BASE_REF branch, but its title suggests otherwise." + exit 1 + fi diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6c54955a2921..d36d29197328 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1,5 +1,14 @@ name: validate +# Default to 'contents: read', which grants actions to read commits. +# +# If any permission is set, any permission not included in the list is +# implicitly set to "none". +# +# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -10,6 +19,7 @@ on: branches: - 'master' - '[0-9]+.[0-9]+' + - '[0-9]+.x' tags: - 'v*' pull_request: diff --git a/.golangci.yml b/.golangci.yml index a00093c80d93..4fab014d4758 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,12 +1,12 @@ linters: enable: - bodyclose + - copyloopvar # Detects places where loop variables are copied. - depguard - dogsled - dupword # Detects duplicate words. - durationcheck - errchkjson - - exportloopref # Detects pointers to enclosing loop variables. - gocritic # Metalinter; detects bugs, performance, and styling issues. - gocyclo - gofumpt # Detects whether code was gofumpt-ed. @@ -54,6 +54,13 @@ linters-settings: desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil gocyclo: 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: enable: - shadow @@ -89,6 +96,10 @@ issues: # The default exclusion rules are a bit too permissive, so copying the relevant ones below 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: - parameter .* always receives @@ -106,6 +117,9 @@ issues: # # 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 + # + # The default list of exclusions can be found at: + # https://golangci-lint.run/usage/false-positives/#default-exclusions # EXC0001 - text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv). is not checked" @@ -123,11 +137,6 @@ issues: - text: "Subprocess launch(ed with variable|ing should be audited)" linters: - gosec - # EXC0008 - # TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close" (gosec) - - text: "G307" - linters: - - gosec # EXC0009 - text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)" linters: @@ -137,26 +146,6 @@ issues: linters: - 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. - text: "package-comments: should have a package comment" linters: diff --git a/Dockerfile b/Dockerfile index 4295e46b17ae..d767921c8ff9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,12 +4,12 @@ ARG BASE_VARIANT=alpine ARG ALPINE_VERSION=3.20 ARG BASE_DEBIAN_DISTRO=bookworm -ARG GO_VERSION=1.21.11 -ARG XX_VERSION=1.4.0 +ARG GO_VERSION=1.22.12 +ARG XX_VERSION=1.6.1 ARG GOVERSIONINFO_VERSION=v1.3.0 ARG GOTESTSUM_VERSION=v1.10.0 -ARG BUILDX_VERSION=0.15.1 -ARG COMPOSE_VERSION=v2.28.0 +ARG BUILDX_VERSION=0.20.0 +ARG COMPOSE_VERSION=v2.32.4 FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx diff --git a/Makefile b/Makefile index 8469c6031813..6b529faef0fb 100644 --- a/Makefile +++ b/Makefile @@ -86,6 +86,16 @@ mod-outdated: ## check outdated dependencies authors: ## generate AUTHORS file from git history scripts/docs/generate-authors.sh +.PHONY: completion +completion: binary +completion: /etc/bash_completion.d/docker +completion: ## generate and install the completion scripts + +.PHONY: /etc/bash_completion.d/docker +/etc/bash_completion.d/docker: ## generate and install the bash-completion script + mkdir -p /etc/bash_completion.d + docker completion bash > /etc/bash_completion.d/docker + .PHONY: manpages manpages: ## generate man pages from go source and markdown scripts/docs/generate-man.sh diff --git a/VERSION b/VERSION index 1678456bf74d..54d1a3453639 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -27.0.1-dev +27.3.1-dev diff --git a/cli-plugins/manager/candidate.go b/cli-plugins/manager/candidate.go index 83e5a05256b1..e65ac1a54f37 100644 --- a/cli-plugins/manager/candidate.go +++ b/cli-plugins/manager/candidate.go @@ -17,5 +17,5 @@ func (c *candidate) Path() string { } func (c *candidate) Metadata() ([]byte, error) { - return exec.Command(c.path, MetadataSubcommandName).Output() + return exec.Command(c.path, MetadataSubcommandName).Output() // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments" } diff --git a/cli-plugins/manager/error.go b/cli-plugins/manager/error.go index f7dc6c7c55cd..cb0bbb5abd47 100644 --- a/cli-plugins/manager/error.go +++ b/cli-plugins/manager/error.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package manager diff --git a/cli-plugins/manager/manager.go b/cli-plugins/manager/manager.go index 9886830240bd..223f3ae0a7fd 100644 --- a/cli-plugins/manager/manager.go +++ b/cli-plugins/manager/manager.go @@ -75,10 +75,12 @@ func getPluginDirs(cfg *configfile.ConfigFile) ([]string, error) { return pluginDirs, nil } -func addPluginCandidatesFromDir(res map[string][]string, d string) error { +func addPluginCandidatesFromDir(res map[string][]string, d string) { dentries, err := os.ReadDir(d) + // Silently ignore any directories which we cannot list (e.g. due to + // permissions or anything else) or which is not a directory if err != nil { - return err + return } for _, dentry := range dentries { switch dentry.Type() & os.ModeType { @@ -99,28 +101,15 @@ func addPluginCandidatesFromDir(res map[string][]string, d string) error { } res[name] = append(res[name], filepath.Join(d, dentry.Name())) } - return nil } // listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority. -func listPluginCandidates(dirs []string) (map[string][]string, error) { +func listPluginCandidates(dirs []string) map[string][]string { result := make(map[string][]string) for _, d := range dirs { - // Silently ignore any directories which we cannot - // Stat (e.g. due to permissions or anything else) or - // which is not a directory. - if fi, err := os.Stat(d); err != nil || !fi.IsDir() { - continue - } - if err := addPluginCandidatesFromDir(result, d); err != nil { - // Silently ignore paths which don't exist. - if os.IsNotExist(err) { - continue - } - return nil, err // Or return partial result? - } + addPluginCandidatesFromDir(result, d) } - return result, nil + return result } // GetPlugin returns a plugin on the system by its name @@ -130,11 +119,7 @@ func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plu return nil, err } - candidates, err := listPluginCandidates(pluginDirs) - if err != nil { - return nil, err - } - + candidates := listPluginCandidates(pluginDirs) if paths, ok := candidates[name]; ok { if len(paths) == 0 { return nil, errPluginNotFound(name) @@ -160,10 +145,7 @@ func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error return nil, err } - candidates, err := listPluginCandidates(pluginDirs) - if err != nil { - return nil, err - } + candidates := listPluginCandidates(pluginDirs) var plugins []Plugin var mu sync.Mutex @@ -240,7 +222,8 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command // TODO: why are we not returning plugin.Err? return nil, errPluginNotFound(name) } - cmd := exec.Command(plugin.Path, args...) + cmd := exec.Command(plugin.Path, args...) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments" + // Using dockerCli.{In,Out,Err}() here results in a hang until something is input. // See: - https://github.com/golang/go/issues/10338 // - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab diff --git a/cli-plugins/manager/manager_test.go b/cli-plugins/manager/manager_test.go index 1583420a4824..92cbd002d4a4 100644 --- a/cli-plugins/manager/manager_test.go +++ b/cli-plugins/manager/manager_test.go @@ -51,8 +51,7 @@ func TestListPluginCandidates(t *testing.T) { dirs = append(dirs, dir.Join(d)) } - candidates, err := listPluginCandidates(dirs) - assert.NilError(t, err) + candidates := listPluginCandidates(dirs) exp := map[string][]string{ "plugin1": { dir.Join("plugins1", "docker-plugin1"), @@ -82,6 +81,29 @@ func TestListPluginCandidates(t *testing.T) { assert.DeepEqual(t, candidates, exp) } +// Regression test for https://github.com/docker/cli/issues/5643. +// Check that inaccessible directories that come before accessible ones are ignored +// and do not prevent the latter from being processed. +func TestListPluginCandidatesInaccesibleDir(t *testing.T) { + dir := fs.NewDir(t, t.Name(), + fs.WithDir("no-perm", fs.WithMode(0)), + fs.WithDir("plugins", + fs.WithFile("docker-buildx", ""), + ), + ) + defer dir.Remove() + + candidates := listPluginCandidates([]string{ + dir.Join("no-perm"), + dir.Join("plugins"), + }) + assert.DeepEqual(t, candidates, map[string][]string{ + "buildx": { + dir.Join("plugins", "docker-buildx"), + }, + }) +} + func TestGetPlugin(t *testing.T) { dir := fs.NewDir(t, t.Name(), fs.WithFile("docker-bbb", ` diff --git a/cli-plugins/manager/plugin.go b/cli-plugins/manager/plugin.go index 877241e0b828..5576ef4301ca 100644 --- a/cli-plugins/manager/plugin.go +++ b/cli-plugins/manager/plugin.go @@ -112,7 +112,7 @@ func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte, return nil, wrapAsPluginError(err, "failed to marshall hook data") } - pCmd := exec.CommandContext(ctx, p.Path, p.Name, HookSubcommandName, string(hDataBytes)) + 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.Env = os.Environ() pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0]) hookCmdOutput, err := pCmd.Output() diff --git a/cli-plugins/socket/socket.go b/cli-plugins/socket/socket.go index 7096b42b2e94..9a76f7a7805c 100644 --- a/cli-plugins/socket/socket.go +++ b/cli-plugins/socket/socket.go @@ -95,6 +95,9 @@ func (pl *PluginServer) Addr() net.Addr { // // The error value is that of the underlying [net.Listner.Close] call. func (pl *PluginServer) Close() error { + if pl == nil { + return nil + } logrus.Trace("Closing plugin server") // Close connections first to ensure the connections get io.EOF instead // of a connection reset. diff --git a/cli-plugins/socket/socket_test.go b/cli-plugins/socket/socket_test.go index df7b35118139..7d786abfc6f9 100644 --- a/cli-plugins/socket/socket_test.go +++ b/cli-plugins/socket/socket_test.go @@ -117,6 +117,18 @@ func TestPluginServer(t *testing.T) { assert.NilError(t, err, "failed to dial returned server") checkDirNoNewPluginServer(t) }) + + t.Run("does not panic on Close if server is nil", func(t *testing.T) { + var srv *PluginServer + defer func() { + if r := recover(); r != nil { + t.Errorf("panicked on Close") + } + }() + + err := srv.Close() + assert.NilError(t, err) + }) } func checkDirNoNewPluginServer(t *testing.T) { diff --git a/cli/command/builder/prune_test.go b/cli/command/builder/prune_test.go index e59e7ad7881e..7fb18900da50 100644 --- a/cli/command/builder/prune_test.go +++ b/cli/command/builder/prune_test.go @@ -3,6 +3,7 @@ package builder import ( "context" "errors" + "io" "testing" "github.com/docker/cli/internal/test" @@ -19,5 +20,7 @@ func TestBuilderPromptTermination(t *testing.T) { }, }) cmd := NewPruneCommand(cli) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) test.TerminatePrompt(ctx, t, cmd, cli) } diff --git a/cli/command/checkpoint/create_test.go b/cli/command/checkpoint/create_test.go index ebee2c1abf4e..3ad7922df64a 100644 --- a/cli/command/checkpoint/create_test.go +++ b/cli/command/checkpoint/create_test.go @@ -42,6 +42,7 @@ func TestCheckpointCreateErrors(t *testing.T) { cmd := newCreateCommand(cli) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/checkpoint/list_test.go b/cli/command/checkpoint/list_test.go index 648d65a7bdc9..7ac4a768de15 100644 --- a/cli/command/checkpoint/list_test.go +++ b/cli/command/checkpoint/list_test.go @@ -42,6 +42,7 @@ func TestCheckpointListErrors(t *testing.T) { cmd := newListCommand(cli) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/checkpoint/remove_test.go b/cli/command/checkpoint/remove_test.go index 9ec1976ae1a0..01bda3c04d2a 100644 --- a/cli/command/checkpoint/remove_test.go +++ b/cli/command/checkpoint/remove_test.go @@ -41,6 +41,7 @@ func TestCheckpointRemoveErrors(t *testing.T) { cmd := newRemoveCommand(cli) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/cli.go b/cli/command/cli.go index 06478c9c776e..da0802d54bcb 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package command @@ -324,7 +324,7 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF if len(configFile.HTTPHeaders) > 0 { opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders)) } - opts = append(opts, client.WithUserAgent(UserAgent())) + opts = append(opts, withCustomHeadersFromEnv(), client.WithUserAgent(UserAgent())) return client.NewClientWithOpts(opts...) } diff --git a/cli/command/cli_options.go b/cli/command/cli_options.go index eb2458768ce3..84b121f34abb 100644 --- a/cli/command/cli_options.go +++ b/cli/command/cli_options.go @@ -2,13 +2,18 @@ package command import ( "context" + "encoding/csv" "io" + "net/http" "os" "strconv" + "strings" "github.com/docker/cli/cli/streams" "github.com/docker/docker/client" + "github.com/docker/docker/errdefs" "github.com/moby/term" + "github.com/pkg/errors" ) // CLIOption is a functional argument to apply options to a [DockerCli]. These @@ -108,3 +113,107 @@ func WithAPIClient(c client.APIClient) CLIOption { return nil } } + +// envOverrideHTTPHeaders is the name of the environment-variable that can be +// used to set custom HTTP headers to be sent by the client. This environment +// variable is the equivalent to the HttpHeaders field in the configuration +// file. +// +// WARNING: If both config and environment-variable are set, the environment +// variable currently overrides all headers set in the configuration file. +// This behavior may change in a future update, as we are considering the +// environment variable to be appending to existing headers (and to only +// override headers with the same name). +// +// While this env-var allows for custom headers to be set, it does not allow +// for built-in headers (such as "User-Agent", if set) to be overridden. +// Also see [client.WithHTTPHeaders] and [client.WithUserAgent]. +// +// This environment variable can be used in situations where headers must be +// set for a specific invocation of the CLI, but should not be set by default, +// and therefore cannot be set in the config-file. +// +// envOverrideHTTPHeaders accepts a comma-separated (CSV) list of key=value pairs, +// where key must be a non-empty, valid MIME header format. Whitespaces surrounding +// the key are trimmed, and the key is normalised. Whitespaces in values are +// preserved, but "key=value" pairs with an empty value (e.g. "key=") are ignored. +// Tuples without a "=" produce an error. +// +// It follows CSV rules for escaping, allowing "key=value" pairs to be quoted +// if they must contain commas, which allows for multiple values for a single +// header to be set. If a key is repeated in the list, later values override +// prior values. +// +// For example, the following value: +// +// one=one-value,"two=two,value","three= a value with whitespace ",four=,five=five=one,five=five-two +// +// Produces four headers (four is omitted as it has an empty value set): +// +// - one (value is "one-value") +// - two (value is "two,value") +// - three (value is " a value with whitespace ") +// - five (value is "five-two", the later value has overridden the prior value) +const envOverrideHTTPHeaders = "DOCKER_CUSTOM_HEADERS" + +// withCustomHeadersFromEnv overriding custom HTTP headers to be sent by the +// client through the [envOverrideHTTPHeaders] environment-variable. This +// environment variable is the equivalent to the HttpHeaders field in the +// configuration file. +// +// WARNING: If both config and environment-variable are set, the environment- +// variable currently overrides all headers set in the configuration file. +// This behavior may change in a future update, as we are considering the +// environment-variable to be appending to existing headers (and to only +// override headers with the same name). +// +// TODO(thaJeztah): this is a client Option, and should be moved to the client. It is non-exported for that reason. +func withCustomHeadersFromEnv() client.Opt { + return func(apiClient *client.Client) error { + value := os.Getenv(envOverrideHTTPHeaders) + if value == "" { + return nil + } + csvReader := csv.NewReader(strings.NewReader(value)) + fields, err := csvReader.Read() + if err != nil { + return errdefs.InvalidParameter(errors.Errorf("failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs", envOverrideHTTPHeaders)) + } + if len(fields) == 0 { + return nil + } + + env := map[string]string{} + for _, kv := range fields { + k, v, hasValue := strings.Cut(kv, "=") + + // Only strip whitespace in keys; preserve whitespace in values. + k = strings.TrimSpace(k) + + if k == "" { + return errdefs.InvalidParameter(errors.Errorf(`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`, envOverrideHTTPHeaders, kv)) + } + + // We don't currently allow empty key=value pairs, and produce an error. + // This is something we could allow in future (e.g. to read value + // from an environment variable with the same name). In the meantime, + // produce an error to prevent users from depending on this. + if !hasValue { + return errdefs.InvalidParameter(errors.Errorf(`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`, envOverrideHTTPHeaders, kv)) + } + + env[http.CanonicalHeaderKey(k)] = v + } + + if len(env) == 0 { + // We should probably not hit this case, as we don't skip values + // (only return errors), but we don't want to discard existing + // headers with an empty set. + return nil + } + + // TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_ + // see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc) + return client.WithHTTPHeaders(env)(apiClient) + } +} diff --git a/cli/command/cli_test.go b/cli/command/cli_test.go index f32513d2cf85..2d12974d9324 100644 --- a/cli/command/cli_test.go +++ b/cli/command/cli_test.go @@ -87,6 +87,41 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) { assert.DeepEqual(t, received, expectedHeaders) } +func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) { + var received http.Header + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + received = r.Header.Clone() + _, _ = w.Write([]byte("OK")) + })) + defer ts.Close() + host := strings.Replace(ts.URL, "http://", "tcp://", 1) + opts := &flags.ClientOptions{Hosts: []string{host}} + configFile := &configfile.ConfigFile{ + HTTPHeaders: map[string]string{ + "My-Header": "Custom-Value from config-file", + }, + } + + // envOverrideHTTPHeaders should override the HTTPHeaders from the config-file, + // so "My-Header" should not be present. + t.Setenv(envOverrideHTTPHeaders, `one=one-value,"two=two,value",three=,four=four-value,four=four-value-override`) + apiClient, err := NewAPIClientFromFlags(opts, configFile) + assert.NilError(t, err) + assert.Equal(t, apiClient.DaemonHost(), host) + assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion) + + expectedHeaders := http.Header{ + "One": []string{"one-value"}, + "Two": []string{"two,value"}, + "Three": []string{""}, + "Four": []string{"four-value-override"}, + "User-Agent": []string{UserAgent()}, + } + _, err = apiClient.Ping(context.Background()) + assert.NilError(t, err) + assert.DeepEqual(t, received, expectedHeaders) +} + func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) { customVersion := "v3.3.3" t.Setenv("DOCKER_API_VERSION", customVersion) diff --git a/cli/command/completion/functions.go b/cli/command/completion/functions.go index 6026ef769299..7ed09a2ab885 100644 --- a/cli/command/completion/functions.go +++ b/cli/command/completion/functions.go @@ -2,6 +2,7 @@ package completion import ( "os" + "strings" "github.com/docker/cli/cli/command/formatter" "github.com/docker/docker/api/types" @@ -59,7 +60,7 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(types for _, ctr := range list { skip := false for _, fn := range filters { - if !fn(ctr) { + if fn != nil && !fn(ctr) { skip = true break } @@ -106,7 +107,86 @@ func NetworkNames(dockerCLI APIClientProvider) ValidArgsFn { } } +// EnvVarNames offers completion for environment-variable names. This +// completion can be used for "--env" and "--build-arg" flags, which +// allow obtaining the value of the given environment-variable if present +// in the local environment, so we only should complete the names of the +// environment variables, and not their value. This also prevents the +// completion script from printing values of environment variables +// containing sensitive values. +// +// For example; +// +// export MY_VAR=hello +// docker run --rm --env MY_VAR alpine printenv MY_VAR +// hello +func EnvVarNames(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) { + envs := os.Environ() + names = make([]string, 0, len(envs)) + for _, env := range envs { + name, _, _ := strings.Cut(env, "=") + names = append(names, name) + } + return names, cobra.ShellCompDirectiveNoFileComp +} + +// FromList offers completion for the given list of options. +func FromList(options ...string) ValidArgsFn { + return cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp) +} + +// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault], +// which indicates to let the shell perform its default behavior after +// completions have been provided. +func FileNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveDefault +} + // NoComplete is used for commands where there's no relevant completion func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveNoFileComp } + +var commonPlatforms = []string{ + "linux", + "linux/386", + "linux/amd64", + "linux/arm", + "linux/arm/v5", + "linux/arm/v6", + "linux/arm/v7", + "linux/arm64", + "linux/arm64/v8", + + // IBM power and z platforms + "linux/ppc64le", + "linux/s390x", + + // Not yet supported + "linux/riscv64", + + "windows", + "windows/amd64", + + "wasip1", + "wasip1/wasm", +} + +// Platforms offers completion for platform-strings. It provides a non-exhaustive +// list of platforms to be used for completion. Platform-strings are based on +// [runtime.GOOS] and [runtime.GOARCH], but with (optional) variants added. A +// list of recognised os/arch combinations from the Go runtime can be obtained +// through "go tool dist list". +// +// Some noteworthy exclusions from this list: +// +// - arm64 images ("windows/arm64", "windows/arm64/v8") do not yet exist for windows. +// - we don't (yet) include `os-variant` for completion (as can be used for Windows images) +// - we don't (yet) include platforms for which we don't build binaries, such as +// BSD platforms (freebsd, netbsd, openbsd), android, macOS (darwin). +// - we currently exclude architectures that may have unofficial builds, +// but don't have wide adoption (and no support), such as loong64, mipsXXX, +// ppc64 (non-le) to prevent confusion. +func Platforms(_ *cobra.Command, _ []string, _ string) (platforms []string, _ cobra.ShellCompDirective) { + return commonPlatforms, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cli/command/completion/functions_test.go b/cli/command/completion/functions_test.go new file mode 100644 index 000000000000..c886d0e4bdb7 --- /dev/null +++ b/cli/command/completion/functions_test.go @@ -0,0 +1,351 @@ +package completion + +import ( + "context" + "errors" + "sort" + "testing" + + "github.com/docker/docker/api/types" + "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" + "gotest.tools/v3/assert" + 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) ([]types.Container, 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) ([]types.Container, error) { + if c.containerListFunc != nil { + return c.containerListFunc(options) + } + return []types.Container{}, 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(types.Container) bool + containers []types.Container + expOut []string + expOpts container.ListOptions + expDirective cobra.ShellCompDirective + }{ + { + doc: "no results", + expDirective: cobra.ShellCompDirectiveNoFileComp, + }, + { + doc: "all containers", + showAll: true, + containers: []types.Container{ + {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: []types.Container{ + {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: []types.Container{ + {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(types.Container) bool{ + func(container types.Container) bool { return container.State == "created" }, + }, + containers: []types.Container{ + {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(types.Container) bool{ + func(container types.Container) bool { return container.ID == "id-a" }, + func(container types.Container) bool { return container.State == "created" }, + }, + containers: []types.Container{ + {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 { + tc := tc + 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) ([]types.Container, 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 { + tc := tc + 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 { + tc := tc + 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) { + values, directives := Platforms(nil, nil, "") + assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion") + 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 { + tc := tc + 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)) + }) + } +} diff --git a/cli/command/config/create_test.go b/cli/command/config/create_test.go index a1c5e618990d..db9f8eac8298 100644 --- a/cli/command/config/create_test.go +++ b/cli/command/config/create_test.go @@ -43,14 +43,18 @@ func TestConfigCreateErrors(t *testing.T) { }, } for _, tc := range testCases { - cmd := newConfigCreateCommand( - test.NewFakeCli(&fakeClient{ - configCreateFunc: tc.configCreateFunc, - }), - ) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.expectedError, func(t *testing.T) { + cmd := newConfigCreateCommand( + test.NewFakeCli(&fakeClient{ + configCreateFunc: tc.configCreateFunc, + }), + ) + cmd.SetArgs(tc.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } diff --git a/cli/command/config/inspect.go b/cli/command/config/inspect.go index 2985823f1dce..7ae4a4c40435 100644 --- a/cli/command/config/inspect.go +++ b/cli/command/config/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package config diff --git a/cli/command/config/inspect_test.go b/cli/command/config/inspect_test.go index a35a74d776d7..fc22516fbba3 100644 --- a/cli/command/config/inspect_test.go +++ b/cli/command/config/inspect_test.go @@ -61,6 +61,7 @@ func TestConfigInspectErrors(t *testing.T) { assert.Check(t, cmd.Flags().Set(key, value)) } cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/config/ls_test.go b/cli/command/config/ls_test.go index 27e76c1a95bb..15658ee3308b 100644 --- a/cli/command/config/ls_test.go +++ b/cli/command/config/ls_test.go @@ -42,6 +42,7 @@ func TestConfigListErrors(t *testing.T) { ) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/config/remove_test.go b/cli/command/config/remove_test.go index bbdd0c4317e9..1466391ee5db 100644 --- a/cli/command/config/remove_test.go +++ b/cli/command/config/remove_test.go @@ -37,6 +37,7 @@ func TestConfigRemoveErrors(t *testing.T) { ) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } @@ -74,6 +75,7 @@ func TestConfigRemoveContinueAfterError(t *testing.T) { cmd := newConfigRemoveCommand(cli) cmd.SetArgs(names) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.Error(t, cmd.Execute(), "error removing config: foo") assert.Check(t, is.DeepEqual(names, removedConfigs)) } diff --git a/cli/command/container/attach.go b/cli/command/container/attach.go index a2d983868355..2d3c2a3b21aa 100644 --- a/cli/command/container/attach.go +++ b/cli/command/container/attach.go @@ -73,7 +73,8 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o apiClient := dockerCLI.Client() // request channel to wait for client - resultC, errC := apiClient.ContainerWait(ctx, containerID, "") + waitCtx := context.WithoutCancel(ctx) + resultC, errC := apiClient.ContainerWait(waitCtx, containerID, "") c, err := inspectContainerAndCheckState(ctx, apiClient, containerID) if err != nil { @@ -146,7 +147,8 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o detachKeys: options.DetachKeys, } - if err := streamer.stream(ctx); err != nil { + // if the context was canceled, this was likely intentional and we shouldn't return an error + if err := streamer.stream(ctx); err != nil && !errors.Is(err, context.Canceled) { return err } @@ -163,6 +165,9 @@ func getExitStatus(errC <-chan error, resultC <-chan container.WaitResponse) err return cli.StatusError{StatusCode: int(result.StatusCode)} } case err := <-errC: + if errors.Is(err, context.Canceled) { + return nil + } return err } diff --git a/cli/command/container/attach_test.go b/cli/command/container/attach_test.go index 7c16aec778f6..dfc293ea8fe0 100644 --- a/cli/command/container/attach_test.go +++ b/cli/command/container/attach_test.go @@ -1,6 +1,7 @@ package container import ( + "context" "io" "testing" @@ -69,19 +70,19 @@ func TestNewAttachCommandErrors(t *testing.T) { }, } for _, tc := range testCases { - cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})) - cmd.SetOut(io.Discard) - cmd.SetArgs(tc.args) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } func TestGetExitStatus(t *testing.T) { - var ( - expectedErr = errors.New("unexpected error") - errC = make(chan error, 1) - resultC = make(chan container.WaitResponse, 1) - ) + expectedErr := errors.New("unexpected error") testcases := []struct { result *container.WaitResponse @@ -109,16 +110,24 @@ func TestGetExitStatus(t *testing.T) { }, expectedError: cli.StatusError{StatusCode: 15}, }, + { + err: context.Canceled, + expectedError: nil, + }, } for _, testcase := range testcases { + errC := make(chan error, 1) + resultC := make(chan container.WaitResponse, 1) if testcase.err != nil { errC <- testcase.err } if testcase.result != nil { resultC <- *testcase.result } + err := getExitStatus(errC, resultC) + if testcase.expectedError == nil { assert.NilError(t, err) } else { diff --git a/cli/command/container/client_test.go b/cli/command/container/client_test.go index 621acbcdd097..193d2aeaffe6 100644 --- a/cli/command/container/client_test.go +++ b/cli/command/container/client_test.go @@ -25,7 +25,7 @@ type fakeClient struct { platform *specs.Platform, containerName string) (container.CreateResponse, error) containerStartFunc func(containerID string, options container.StartOptions) error - imageCreateFunc func(parentReference string, options image.CreateOptions) (io.ReadCloser, error) + imageCreateFunc func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) infoFunc func() (system.Info, error) containerStatPathFunc func(containerID, path string) (container.PathStat, error) containerCopyFromFunc func(containerID, srcPath string) (io.ReadCloser, container.PathStat, error) @@ -94,9 +94,9 @@ func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, op return nil } -func (f *fakeClient) ImageCreate(_ context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) { +func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) { if f.imageCreateFunc != nil { - return f.imageCreateFunc(parentReference, options) + return f.imageCreateFunc(ctx, parentReference, options) } return nil, nil } diff --git a/cli/command/container/completion.go b/cli/command/container/completion.go new file mode 100644 index 000000000000..552fbecff76b --- /dev/null +++ b/cli/command/container/completion.go @@ -0,0 +1,336 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.22 +// +build go1.22 + +package container + +import ( + "strings" + "sync" + + "github.com/docker/cli/cli/command/completion" + "github.com/docker/docker/api/types/container" + "github.com/moby/sys/capability" + "github.com/moby/sys/signal" + "github.com/spf13/cobra" +) + +// allCaps is the magic value for "all capabilities". +const allCaps = "ALL" + +// allLinuxCapabilities is a list of all known Linux capabilities. +// +// TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true") +// TODO(thaJeztah): consider what casing we want to use for completion (see below); +// +// We need to consider what format is most convenient; currently we use the +// canonical name (uppercase and "CAP_" prefix), however, tab-completion is +// case-sensitive by default, so requires the user to type uppercase letters +// to filter the list of options. +// +// Bash completion provides a `completion-ignore-case on` option to make completion +// case-insensitive (https://askubuntu.com/a/87066), but it looks to be a global +// option; the current cobra.CompletionOptions also don't provide this as an option +// to be used in the generated completion-script. +// +// Fish completion has `smartcase` (by default?) which matches any case if +// all of the input is lowercase. +// +// Zsh does not appear have a dedicated option, but allows setting matching-rules +// (see https://superuser.com/a/1092328). +var allLinuxCapabilities = sync.OnceValue(func() []string { + caps := capability.ListKnown() + out := make([]string, 0, len(caps)+1) + out = append(out, allCaps) + for _, c := range caps { + out = append(out, "CAP_"+strings.ToUpper(c.String())) + } + return out +}) + +// logDriverOptions provides the options for each built-in logging driver. +var logDriverOptions = map[string][]string{ + "awslogs": { + "max-buffer-size", "mode", "awslogs-create-group", "awslogs-credentials-endpoint", "awslogs-datetime-format", + "awslogs-group", "awslogs-multiline-pattern", "awslogs-region", "awslogs-stream", "tag", + }, + "fluentd": { + "max-buffer-size", "mode", "env", "env-regex", "labels", "fluentd-address", "fluentd-async", + "fluentd-buffer-limit", "fluentd-request-ack", "fluentd-retry-wait", "fluentd-max-retries", + "fluentd-sub-second-precision", "tag", + }, + "gcplogs": { + "max-buffer-size", "mode", "env", "env-regex", "labels", "gcp-log-cmd", "gcp-meta-id", "gcp-meta-name", + "gcp-meta-zone", "gcp-project", + }, + "gelf": { + "max-buffer-size", "mode", "env", "env-regex", "labels", "gelf-address", "gelf-compression-level", + "gelf-compression-type", "gelf-tcp-max-reconnect", "gelf-tcp-reconnect-delay", "tag", + }, + "journald": {"max-buffer-size", "mode", "env", "env-regex", "labels", "tag"}, + "json-file": {"max-buffer-size", "mode", "env", "env-regex", "labels", "compress", "max-file", "max-size"}, + "local": {"max-buffer-size", "mode", "compress", "max-file", "max-size"}, + "none": {}, + "splunk": { + "max-buffer-size", "mode", "env", "env-regex", "labels", "splunk-caname", "splunk-capath", "splunk-format", + "splunk-gzip", "splunk-gzip-level", "splunk-index", "splunk-insecureskipverify", "splunk-source", + "splunk-sourcetype", "splunk-token", "splunk-url", "splunk-verify-connection", "tag", + }, + "syslog": { + "max-buffer-size", "mode", "env", "env-regex", "labels", "syslog-address", "syslog-facility", "syslog-format", + "syslog-tls-ca-cert", "syslog-tls-cert", "syslog-tls-key", "syslog-tls-skip-verify", "tag", + }, +} + +// builtInLogDrivers provides a list of the built-in logging drivers. +var builtInLogDrivers = sync.OnceValue(func() []string { + drivers := make([]string, 0, len(logDriverOptions)) + for driver := range logDriverOptions { + drivers = append(drivers, driver) + } + return drivers +}) + +// allLogDriverOptions provides all options of the built-in logging drivers. +// The list does not contain duplicates. +var allLogDriverOptions = sync.OnceValue(func() []string { + var result []string + seen := make(map[string]bool) + for driver := range logDriverOptions { + for _, opt := range logDriverOptions[driver] { + if !seen[opt] { + seen[opt] = true + result = append(result, opt) + } + } + } + return result +}) + +// restartPolicies is a list of all valid restart-policies.. +// +// TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true") +var restartPolicies = []string{ + string(container.RestartPolicyDisabled), + string(container.RestartPolicyAlways), + string(container.RestartPolicyOnFailure), + string(container.RestartPolicyUnlessStopped), +} + +// addCompletions adds the completions that `run` and `create` have in common. +func addCompletions(cmd *cobra.Command, dockerCLI completion.APIClientProvider) { + _ = cmd.RegisterFlagCompletionFunc("attach", completion.FromList("stderr", "stdin", "stdout")) + _ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames) + _ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames) + _ = cmd.RegisterFlagCompletionFunc("cgroupns", completeCgroupns()) + _ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames) + _ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames) + _ = cmd.RegisterFlagCompletionFunc("ipc", completeIpc(dockerCLI)) + _ = cmd.RegisterFlagCompletionFunc("link", completeLink(dockerCLI)) + _ = cmd.RegisterFlagCompletionFunc("log-driver", completeLogDriver(dockerCLI)) + _ = cmd.RegisterFlagCompletionFunc("log-opt", completeLogOpt) + _ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCLI)) + _ = cmd.RegisterFlagCompletionFunc("pid", completePid(dockerCLI)) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) + _ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever)) + _ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies) + _ = cmd.RegisterFlagCompletionFunc("security-opt", completeSecurityOpt) + _ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals) + _ = cmd.RegisterFlagCompletionFunc("storage-opt", completeStorageOpt) + _ = cmd.RegisterFlagCompletionFunc("ulimit", completeUlimit) + _ = cmd.RegisterFlagCompletionFunc("userns", completion.FromList("host")) + _ = cmd.RegisterFlagCompletionFunc("uts", completion.FromList("host")) + _ = cmd.RegisterFlagCompletionFunc("volume-driver", completeVolumeDriver(dockerCLI)) + _ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCLI, true)) +} + +// completeCgroupns implements shell completion for the `--cgroupns` option of `run` and `create`. +func completeCgroupns() completion.ValidArgsFn { + return completion.FromList(string(container.CgroupnsModeHost), string(container.CgroupnsModePrivate)) +} + +// completeDetachKeys implements shell completion for the `--detach-keys` option of `run` and `create`. +func completeDetachKeys(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return []string{"ctrl-"}, cobra.ShellCompDirectiveNoSpace +} + +// completeIpc implements shell completion for the `--ipc` option of `run` and `create`. +// The completion is partly composite. +func completeIpc(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container" + return []string{"container:"}, cobra.ShellCompDirectiveNoSpace + } + if strings.HasPrefix(toComplete, "container:") { + names, _ := completion.ContainerNames(dockerCLI, true)(cmd, args, toComplete) + return prefixWith("container:", names), cobra.ShellCompDirectiveNoFileComp + } + return []string{ + string(container.IPCModeContainer + ":"), + string(container.IPCModeHost), + string(container.IPCModeNone), + string(container.IPCModePrivate), + string(container.IPCModeShareable), + }, cobra.ShellCompDirectiveNoFileComp + } +} + +// completeLink implements shell completion for the `--link` option of `run` and `create`. +func completeLink(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return postfixWith(":", containerNames(dockerCLI, cmd, args, toComplete)), cobra.ShellCompDirectiveNoSpace + } +} + +// completeLogDriver implements shell completion for the `--log-driver` option of `run` and `create`. +// The log drivers are collected from a call to the Info endpoint with a fallback to a hard-coded list +// of the build-in log drivers. +func completeLogDriver(dockerCLI completion.APIClientProvider) completion.ValidArgsFn { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + info, err := dockerCLI.Client().Info(cmd.Context()) + if err != nil { + return builtInLogDrivers(), cobra.ShellCompDirectiveNoFileComp + } + drivers := info.Plugins.Log + return drivers, cobra.ShellCompDirectiveNoFileComp + } +} + +// completeLogOpt implements shell completion for the `--log-opt` option of `run` and `create`. +// If the user supplied a log-driver, only options for that driver are returned. +func completeLogOpt(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + driver, _ := cmd.Flags().GetString("log-driver") + if options, exists := logDriverOptions[driver]; exists { + return postfixWith("=", options), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp + } + return postfixWith("=", allLogDriverOptions()), cobra.ShellCompDirectiveNoSpace +} + +// completePid implements shell completion for the `--pid` option of `run` and `create`. +func completePid(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container" + return []string{"container:"}, cobra.ShellCompDirectiveNoSpace + } + if strings.HasPrefix(toComplete, "container:") { + names, _ := completion.ContainerNames(dockerCLI, true)(cmd, args, toComplete) + return prefixWith("container:", names), cobra.ShellCompDirectiveNoFileComp + } + return []string{"container:", "host"}, cobra.ShellCompDirectiveNoFileComp + } +} + +// completeSecurityOpt implements shell completion for the `--security-opt` option of `run` and `create`. +// The completion is partly composite. +func completeSecurityOpt(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(toComplete) > 0 && strings.HasPrefix("apparmor=", toComplete) { //nolint:gocritic // not swapped, matches partly typed "apparmor=" + return []string{"apparmor="}, cobra.ShellCompDirectiveNoSpace + } + if len(toComplete) > 0 && strings.HasPrefix("label", toComplete) { //nolint:gocritic // not swapped, matches partly typed "label" + return []string{"label="}, cobra.ShellCompDirectiveNoSpace + } + if strings.HasPrefix(toComplete, "label=") { + if strings.HasPrefix(toComplete, "label=d") { + return []string{"label=disable"}, cobra.ShellCompDirectiveNoFileComp + } + labels := []string{"disable", "level:", "role:", "type:", "user:"} + return prefixWith("label=", labels), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp + } + // length must be > 1 here so that completion of "s" falls through. + if len(toComplete) > 1 && strings.HasPrefix("seccomp", toComplete) { //nolint:gocritic // not swapped, matches partly typed "seccomp" + return []string{"seccomp="}, cobra.ShellCompDirectiveNoSpace + } + if strings.HasPrefix(toComplete, "seccomp=") { + return []string{"seccomp=unconfined"}, cobra.ShellCompDirectiveNoFileComp + } + return []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"}, cobra.ShellCompDirectiveNoFileComp +} + +// completeStorageOpt implements shell completion for the `--storage-opt` option of `run` and `create`. +func completeStorageOpt(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return []string{"size="}, cobra.ShellCompDirectiveNoSpace +} + +// completeUlimit implements shell completion for the `--ulimit` option of `run` and `create`. +func completeUlimit(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + limits := []string{ + "as", + "chroot", + "core", + "cpu", + "data", + "fsize", + "locks", + "maxlogins", + "maxsyslogins", + "memlock", + "msgqueue", + "nice", + "nofile", + "nproc", + "priority", + "rss", + "rtprio", + "sigpending", + "stack", + } + return postfixWith("=", limits), cobra.ShellCompDirectiveNoSpace +} + +// completeVolumeDriver contacts the API to get the built-in and installed volume drivers. +func completeVolumeDriver(dockerCLI completion.APIClientProvider) completion.ValidArgsFn { + return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + info, err := dockerCLI.Client().Info(cmd.Context()) + if err != nil { + // fallback: the built-in drivers + return []string{"local"}, cobra.ShellCompDirectiveNoFileComp + } + drivers := info.Plugins.Volume + return drivers, cobra.ShellCompDirectiveNoFileComp + } +} + +// 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 +} + +// 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 +} + +func completeLinuxCapabilityNames(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) { + return completion.FromList(allLinuxCapabilities()...)(cmd, args, toComplete) +} + +func completeRestartPolicies(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) { + return completion.FromList(restartPolicies...)(cmd, args, toComplete) +} + +func completeSignals(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) { + // TODO(thaJeztah): do we want to provide the full list here, or a subset? + signalNames := make([]string, 0, len(signal.SignalMap)) + for k := range signal.SignalMap { + signalNames = append(signalNames, k) + } + return completion.FromList(signalNames...)(cmd, args, toComplete) +} diff --git a/cli/command/container/completion_test.go b/cli/command/container/completion_test.go new file mode 100644 index 000000000000..b4ba399903db --- /dev/null +++ b/cli/command/container/completion_test.go @@ -0,0 +1,135 @@ +package container + +import ( + "strings" + "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/moby/sys/signal" + "github.com/spf13/cobra" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestCompleteLinuxCapabilityNames(t *testing.T) { + names, directives := completeLinuxCapabilityNames(nil, nil, "") + assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion") + assert.Assert(t, len(names) > 1) + assert.Check(t, names[0] == allCaps) + for _, name := range names[1:] { + assert.Check(t, strings.HasPrefix(name, "CAP_")) + assert.Check(t, is.Equal(name, strings.ToUpper(name)), "Should be formatted uppercase") + } +} + +func TestCompletePid(t *testing.T) { + tests := []struct { + containerListFunc func(container.ListOptions) ([]types.Container, error) + toComplete string + expectedCompletions []string + expectedDirective cobra.ShellCompDirective + }{ + { + toComplete: "", + expectedCompletions: []string{"container:", "host"}, + expectedDirective: cobra.ShellCompDirectiveNoFileComp, + }, + { + toComplete: "c", + expectedCompletions: []string{"container:"}, + expectedDirective: cobra.ShellCompDirectiveNoSpace, + }, + { + containerListFunc: func(container.ListOptions) ([]types.Container, error) { + return []types.Container{ + *builders.Container("c1"), + *builders.Container("c2"), + }, nil + }, + toComplete: "container:", + expectedCompletions: []string{"container:c1", "container:c2"}, + expectedDirective: cobra.ShellCompDirectiveNoFileComp, + }, + } + + for _, tc := range tests { + t.Run(tc.toComplete, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + containerListFunc: tc.containerListFunc, + }) + completions, directive := completePid(cli)(NewRunCommand(cli), nil, tc.toComplete) + assert.Check(t, is.DeepEqual(completions, tc.expectedCompletions)) + assert.Check(t, is.Equal(directive, tc.expectedDirective)) + }) + } +} + +func TestCompleteRestartPolicies(t *testing.T) { + values, directives := completeRestartPolicies(nil, nil, "") + assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion") + expected := restartPolicies + assert.Check(t, is.DeepEqual(values, expected)) +} + +func TestCompleteSecurityOpt(t *testing.T) { + tests := []struct { + toComplete string + expectedCompletions []string + expectedDirective cobra.ShellCompDirective + }{ + { + toComplete: "", + expectedCompletions: []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"}, + expectedDirective: cobra.ShellCompDirectiveNoFileComp, + }, + { + toComplete: "apparmor=", + expectedCompletions: []string{"apparmor="}, + expectedDirective: cobra.ShellCompDirectiveNoSpace, + }, + { + toComplete: "label=", + expectedCompletions: []string{"label=disable", "label=level:", "label=role:", "label=type:", "label=user:"}, + expectedDirective: cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp, + }, + { + toComplete: "s", + // We do not filter matching completions but delegate this task to the shell script. + expectedCompletions: []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"}, + expectedDirective: cobra.ShellCompDirectiveNoFileComp, + }, + { + toComplete: "se", + expectedCompletions: []string{"seccomp="}, + expectedDirective: cobra.ShellCompDirectiveNoSpace, + }, + { + toComplete: "seccomp=", + expectedCompletions: []string{"seccomp=unconfined"}, + expectedDirective: cobra.ShellCompDirectiveNoFileComp, + }, + { + toComplete: "sy", + expectedCompletions: []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"}, + expectedDirective: cobra.ShellCompDirectiveNoFileComp, + }, + } + + for _, tc := range tests { + t.Run(tc.toComplete, func(t *testing.T) { + completions, directive := completeSecurityOpt(nil, nil, tc.toComplete) + assert.Check(t, is.DeepEqual(completions, tc.expectedCompletions)) + assert.Check(t, is.Equal(directive, tc.expectedDirective)) + }) + } +} + +func TestCompleteSignals(t *testing.T) { + values, directives := completeSignals(nil, nil, "") + assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion") + assert.Check(t, len(values) > 1) + assert.Check(t, is.Len(values, len(signal.SignalMap))) +} diff --git a/cli/command/container/cp.go b/cli/command/container/cp.go index b361e2678cdc..d6fe10341a21 100644 --- a/cli/command/container/cp.go +++ b/cli/command/container/cp.go @@ -17,7 +17,6 @@ import ( "github.com/docker/cli/cli/streams" "github.com/docker/docker/api/types/container" "github.com/docker/docker/pkg/archive" - "github.com/docker/docker/pkg/system" units "github.com/docker/go-units" "github.com/morikuni/aec" "github.com/pkg/errors" @@ -235,7 +234,7 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cp // If the destination is a symbolic link, we should follow it. if err == nil && srcStat.Mode&os.ModeSymlink != 0 { linkTarget := srcStat.LinkTarget - if !system.IsAbs(linkTarget) { + if !isAbs(linkTarget) { // Join with the parent directory. srcParent, _ := archive.SplitPathDirEntry(srcPath) linkTarget = filepath.Join(srcParent, linkTarget) @@ -319,7 +318,7 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpCo // If the destination is a symbolic link, we should evaluate it. if err == nil && dstStat.Mode&os.ModeSymlink != 0 { linkTarget := dstStat.LinkTarget - if !system.IsAbs(linkTarget) { + if !isAbs(linkTarget) { // Join with the parent directory. dstParent, _ := archive.SplitPathDirEntry(dstPath) linkTarget = filepath.Join(dstParent, linkTarget) @@ -434,7 +433,7 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpCo // client, a `:` could be part of an absolute Windows path, in which case it // is immediately proceeded by a backslash. func splitCpArg(arg string) (ctr, path string) { - if system.IsAbs(arg) { + if isAbs(arg) { // Explicit local absolute path, e.g., `C:\foo` or `/foo`. return "", arg } @@ -448,3 +447,15 @@ func splitCpArg(arg string) (ctr, path string) { return ctr, path } + +// IsAbs is a platform-agnostic wrapper for filepath.IsAbs. +// +// On Windows, golang filepath.IsAbs does not consider a path \windows\system32 +// as absolute as it doesn't start with a drive-letter/colon combination. However, +// in docker we need to verify things such as WORKDIR /windows/system32 in +// a Dockerfile (which gets translated to \windows\system32 when being processed +// by the daemon). This SHOULD be treated as absolute from a docker processing +// perspective. +func isAbs(path string) bool { + return filepath.IsAbs(path) || strings.HasPrefix(path, string(os.PathSeparator)) +} diff --git a/cli/command/container/cp_test.go b/cli/command/container/cp_test.go index 245aef3a5a50..30de39adf467 100644 --- a/cli/command/container/cp_test.go +++ b/cli/command/container/cp_test.go @@ -67,14 +67,15 @@ func TestRunCopyFromContainerToStdout(t *testing.T) { } func TestRunCopyFromContainerToFilesystem(t *testing.T) { - destDir := fs.NewDir(t, "cp-test", + srcDir := fs.NewDir(t, "cp-test", fs.WithFile("file1", "content\n")) - defer destDir.Remove() + + destDir := fs.NewDir(t, "cp-test") cli := test.NewFakeCli(&fakeClient{ containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) { assert.Check(t, is.Equal("container", ctr)) - readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{}) + readCloser, err := archive.Tar(srcDir.Path(), archive.Uncompressed) return readCloser, container.PathStat{}, err }, }) @@ -178,13 +179,14 @@ func TestSplitCpArg(t *testing.T) { expectedContainer: "container", }, } - for _, testcase := range testcases { - t.Run(testcase.doc, func(t *testing.T) { - skip.If(t, testcase.os != "" && testcase.os != runtime.GOOS) - - ctr, path := splitCpArg(testcase.path) - assert.Check(t, is.Equal(testcase.expectedContainer, ctr)) - assert.Check(t, is.Equal(testcase.expectedPath, path)) + for _, tc := range testcases { + tc := tc + t.Run(tc.doc, func(t *testing.T) { + skip.If(t, tc.os == "windows" && runtime.GOOS != "windows" || tc.os == "linux" && runtime.GOOS == "windows") + + ctr, path := splitCpArg(tc.path) + assert.Check(t, is.Equal(tc.expectedContainer, ctr)) + assert.Check(t, is.Equal(tc.expectedPath, path)) }) } } diff --git a/cli/command/container/create.go b/cli/command/container/create.go index 3193ccf20072..b5965b98016a 100644 --- a/cli/command/container/create.go +++ b/cli/command/container/create.go @@ -77,6 +77,16 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command { command.AddPlatformFlag(flags, &options.platform) command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled()) copts = addFlags(flags) + + addCompletions(cmd, dockerCli) + + flags.VisitAll(func(flag *pflag.Flag) { + // Set a default completion function if none was set. We don't look + // up if it does already have one set, because Cobra does this for + // us, and returns an error (which we ignore for this reason). + _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + }) + return cmd } diff --git a/cli/command/container/create_test.go b/cli/command/container/create_test.go index f77c699db344..f8c5a9bc14b4 100644 --- a/cli/command/container/create_test.go +++ b/cli/command/container/create_test.go @@ -133,7 +133,7 @@ func TestCreateContainerImagePullPolicy(t *testing.T) { return container.CreateResponse{ID: containerID}, nil } }, - imageCreateFunc: func(parentReference string, options image.CreateOptions) (io.ReadCloser, error) { + imageCreateFunc: func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) { defer func() { pullCounter++ }() return io.NopCloser(strings.NewReader("")), nil }, @@ -236,6 +236,7 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) { fakeCLI.SetNotaryClient(tc.notaryFunc) cmd := NewCreateCommand(fakeCLI) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) err := cmd.Execute() assert.ErrorContains(t, err, tc.expectedError) diff --git a/cli/command/container/exec.go b/cli/command/container/exec.go index 1c0a741263cf..8244734a5cb7 100644 --- a/cli/command/container/exec.go +++ b/cli/command/container/exec.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "os" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -79,12 +78,8 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command { flags.StringVarP(&options.Workdir, "workdir", "w", "", "Working directory inside the container") flags.SetAnnotation("workdir", "version", []string{"1.35"}) - _ = cmd.RegisterFlagCompletionFunc("env", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { - return os.Environ(), cobra.ShellCompDirectiveNoFileComp - }) - _ = cmd.RegisterFlagCompletionFunc("env-file", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { - return nil, cobra.ShellCompDirectiveDefault // _filedir - }) + _ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames) + _ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames) return cmd } diff --git a/cli/command/container/export_test.go b/cli/command/container/export_test.go index 891317fff3df..ae21a8799533 100644 --- a/cli/command/container/export_test.go +++ b/cli/command/container/export_test.go @@ -39,6 +39,7 @@ func TestContainerExportOutputToIrregularFile(t *testing.T) { }) cmd := NewExportCommand(cli) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) cmd.SetArgs([]string{"-o", "/dev/random", "container"}) err := cmd.Execute() diff --git a/cli/command/container/inspect.go b/cli/command/container/inspect.go index 1f404ac7ea73..23b33b59fda2 100644 --- a/cli/command/container/inspect.go +++ b/cli/command/container/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package container diff --git a/cli/command/container/kill.go b/cli/command/container/kill.go index 96ce22e5ae85..0095198b5aab 100644 --- a/cli/command/container/kill.go +++ b/cli/command/container/kill.go @@ -38,6 +38,9 @@ func NewKillCommand(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container") + + _ = cmd.RegisterFlagCompletionFunc("signal", completeSignals) + return cmd } @@ -50,7 +53,7 @@ func runKill(ctx context.Context, dockerCli command.Cli, opts *killOptions) erro if err := <-errChan; err != nil { errs = append(errs, err.Error()) } else { - fmt.Fprintln(dockerCli.Out(), name) + _, _ = fmt.Fprintln(dockerCli.Out(), name) } } if len(errs) > 0 { diff --git a/cli/command/container/list_test.go b/cli/command/container/list_test.go index 6c80541870ef..6a921562c9e4 100644 --- a/cli/command/container/list_test.go +++ b/cli/command/container/list_test.go @@ -128,7 +128,6 @@ func TestContainerListBuildContainerListOptions(t *testing.T) { func TestContainerListErrors(t *testing.T) { testCases := []struct { - args []string flags map[string]string containerListFunc func(container.ListOptions) ([]types.Container, error) expectedError string @@ -158,11 +157,12 @@ func TestContainerListErrors(t *testing.T) { containerListFunc: tc.containerListFunc, }), ) - cmd.SetArgs(tc.args) for key, value := range tc.flags { assert.Check(t, cmd.Flags().Set(key, value)) } + cmd.SetArgs([]string{}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } @@ -180,6 +180,9 @@ func TestContainerListWithoutFormat(t *testing.T) { }, }) cmd := newListCommand(cli) + cmd.SetArgs([]string{}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.NilError(t, cmd.Execute()) golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format.golden") } @@ -194,6 +197,9 @@ func TestContainerListNoTrunc(t *testing.T) { }, }) cmd := newListCommand(cli) + cmd.SetArgs([]string{}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.Check(t, cmd.Flags().Set("no-trunc", "true")) assert.NilError(t, cmd.Execute()) golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format-no-trunc.golden") @@ -210,6 +216,9 @@ func TestContainerListNamesMultipleTime(t *testing.T) { }, }) cmd := newListCommand(cli) + cmd.SetArgs([]string{}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.Check(t, cmd.Flags().Set("format", "{{.Names}} {{.Names}}")) assert.NilError(t, cmd.Execute()) golden.Assert(t, cli.OutBuffer().String(), "container-list-format-name-name.golden") @@ -226,6 +235,9 @@ func TestContainerListFormatTemplateWithArg(t *testing.T) { }, }) 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.NilError(t, cmd.Execute()) golden.Assert(t, cli.OutBuffer().String(), "container-list-format-with-arg.golden") @@ -275,6 +287,9 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) { }, }) cmd := newListCommand(cli) + cmd.SetArgs([]string{}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.Check(t, cmd.Flags().Set("format", tc.format)) if tc.sizeFlag != "" { assert.Check(t, cmd.Flags().Set("size", tc.sizeFlag)) @@ -297,6 +312,9 @@ func TestContainerListWithConfigFormat(t *testing.T) { PsFormat: "{{ .Names }} {{ .Image }} {{ .Labels }} {{ .Size}}", }) cmd := newListCommand(cli) + cmd.SetArgs([]string{}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.NilError(t, cmd.Execute()) golden.Assert(t, cli.OutBuffer().String(), "container-list-with-config-format.golden") } @@ -314,6 +332,9 @@ func TestContainerListWithFormat(t *testing.T) { t.Run("with format", func(t *testing.T) { cli.OutBuffer().Reset() 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.NilError(t, cmd.Execute()) golden.Assert(t, cli.OutBuffer().String(), "container-list-with-format.golden") @@ -322,6 +343,9 @@ func TestContainerListWithFormat(t *testing.T) { t.Run("with format and quiet", func(t *testing.T) { cli.OutBuffer().Reset() 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("quiet", "true")) assert.NilError(t, cmd.Execute()) diff --git a/cli/command/container/opts_test.go b/cli/command/container/opts_test.go index e03c26530e2f..0f835ee44770 100644 --- a/cli/command/container/opts_test.go +++ b/cli/command/container/opts_test.go @@ -921,7 +921,7 @@ func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) { } // UTF16 with BOM - e := "contains invalid utf8 bytes at line" + e := "invalid utf8 bytes at line" if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) { t.Fatalf("Expected an error with message '%s', got %v", e, err) } diff --git a/cli/command/container/prune_test.go b/cli/command/container/prune_test.go index ec006f9a39bd..1ded0f288242 100644 --- a/cli/command/container/prune_test.go +++ b/cli/command/container/prune_test.go @@ -2,6 +2,7 @@ package container import ( "context" + "io" "testing" "github.com/docker/cli/internal/test" @@ -20,5 +21,8 @@ func TestContainerPrunePromptTermination(t *testing.T) { }, }) cmd := NewPruneCommand(cli) + cmd.SetArgs([]string{}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) test.TerminatePrompt(ctx, t, cmd, cli) } diff --git a/cli/command/container/restart.go b/cli/command/container/restart.go index 0ac4fa985ef7..8e7c0d4a026f 100644 --- a/cli/command/container/restart.go +++ b/cli/command/container/restart.go @@ -43,6 +43,9 @@ func NewRestartCommand(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container") flags.IntVarP(&opts.timeout, "time", "t", 0, "Seconds to wait before killing the container") + + _ = cmd.RegisterFlagCompletionFunc("signal", completeSignals) + return cmd } diff --git a/cli/command/container/rm.go b/cli/command/container/rm.go index 171e8e81e9e4..b234b6009874 100644 --- a/cli/command/container/rm.go +++ b/cli/command/container/rm.go @@ -8,6 +8,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/errdefs" "github.com/pkg/errors" @@ -38,7 +39,9 @@ func NewRmCommand(dockerCli command.Cli) *cobra.Command { Annotations: map[string]string{ "aliases": "docker container rm, docker container remove, docker rm", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, true), + ValidArgsFunction: completion.ContainerNames(dockerCli, true, func(ctr types.Container) bool { + return opts.force || ctr.State == "exited" || ctr.State == "created" + }), } flags := cmd.Flags() diff --git a/cli/command/container/rm_test.go b/cli/command/container/rm_test.go index 704effbb59ce..493a2a68ba10 100644 --- a/cli/command/container/rm_test.go +++ b/cli/command/container/rm_test.go @@ -45,6 +45,7 @@ func TestRemoveForce(t *testing.T) { }) cmd := NewRmCommand(cli) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) err := cmd.Execute() diff --git a/cli/command/container/run.go b/cli/command/container/run.go index 749071b17332..bd7afb9d3a7c 100644 --- a/cli/command/container/run.go +++ b/cli/command/container/run.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "os" "strings" "syscall" @@ -70,22 +69,16 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command { command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled()) copts = addFlags(flags) - cmd.RegisterFlagCompletionFunc( - "env", - func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return os.Environ(), cobra.ShellCompDirectiveNoFileComp - }, - ) - cmd.RegisterFlagCompletionFunc( - "env-file", - func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return nil, cobra.ShellCompDirectiveDefault - }, - ) - cmd.RegisterFlagCompletionFunc( - "network", - completion.NetworkNames(dockerCli), - ) + _ = cmd.RegisterFlagCompletionFunc("detach-keys", completeDetachKeys) + addCompletions(cmd, dockerCli) + + flags.VisitAll(func(flag *pflag.Flag) { + // Set a default completion function if none was set. We don't look + // up if it does already have one set, because Cobra does this for + // us, and returns an error (which we ignore for this reason). + _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + }) + return cmd } @@ -140,9 +133,6 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption config.StdinOnce = false } - ctx, cancelFun := context.WithCancel(ctx) - defer cancelFun() - containerID, err := createContainer(ctx, dockerCli, containerCfg, &runOpts.createOptions) if err != nil { reportError(stderr, "run", err.Error(), true) @@ -159,6 +149,9 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption defer signal.StopCatch(sigc) } + ctx, cancelFun := context.WithCancel(context.WithoutCancel(ctx)) + defer cancelFun() + var ( waitDisplayID chan struct{} errCh chan error @@ -178,6 +171,9 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption detachKeys = runOpts.detachKeys } + // ctx should not be cancellable here, as this would kill the stream to the container + // and we want to keep the stream open until the process in the container exits or until + // the user forcefully terminates the CLI. closeFn, err := attachContainer(ctx, dockerCli, containerID, &errCh, config, container.AttachOptions{ Stream: true, Stdin: config.AttachStdin, diff --git a/cli/command/container/run_test.go b/cli/command/container/run_test.go index d02956da1de3..9a1569be9210 100644 --- a/cli/command/container/run_test.go +++ b/cli/command/container/run_test.go @@ -2,10 +2,11 @@ package container import ( "context" + "encoding/json" "errors" + "fmt" "io" "net" - "os/signal" "syscall" "testing" "time" @@ -17,7 +18,9 @@ import ( "github.com/docker/cli/internal/test/notary" "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/pkg/jsonmessage" specs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/pflag" "gotest.tools/v3/assert" @@ -38,15 +41,85 @@ func TestRunLabel(t *testing.T) { assert.NilError(t, cmd.Execute()) } -func TestRunAttachTermination(t *testing.T) { +func TestRunAttach(t *testing.T) { p, tty, err := pty.Open() assert.NilError(t, err) + defer func() { + _ = tty.Close() + _ = p.Close() + }() + + var conn net.Conn + attachCh := make(chan struct{}) + fakeCLI := test.NewFakeCli(&fakeClient{ + createContainerFunc: func(_ *container.Config, _ *container.HostConfig, _ *network.NetworkingConfig, _ *specs.Platform, _ string) (container.CreateResponse, error) { + return container.CreateResponse{ + ID: "id", + }, nil + }, + containerAttachFunc: func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) { + server, client := net.Pipe() + conn = server + t.Cleanup(func() { + _ = server.Close() + }) + attachCh <- struct{}{} + return types.NewHijackedResponse(client, types.MediaTypeRawStream), nil + }, + waitFunc: func(_ string) (<-chan container.WaitResponse, <-chan error) { + responseChan := make(chan container.WaitResponse, 1) + errChan := make(chan error) + + responseChan <- container.WaitResponse{ + StatusCode: 33, + } + return responseChan, errChan + }, + // use new (non-legacy) wait API + // see: 38591f20d07795aaef45d400df89ca12f29c603b + Version: "1.30", + }, func(fc *test.FakeCli) { + fc.SetOut(streams.NewOut(tty)) + fc.SetIn(streams.NewIn(tty)) + }) + + cmd := NewRunCommand(fakeCLI) + cmd.SetArgs([]string{"-it", "busybox"}) + cmd.SilenceUsage = true + cmdErrC := make(chan error, 1) + go func() { + cmdErrC <- cmd.Execute() + }() + // run command should attempt to attach to the container + select { + case <-time.After(5 * time.Second): + t.Fatal("containerAttachFunc was not called before the 5 second timeout") + case <-attachCh: + } + + // end stream from "container" so that we'll detach + conn.Close() + + select { + case cmdErr := <-cmdErrC: + assert.Equal(t, cmdErr, cli.StatusError{ + StatusCode: 33, + }) + case <-time.After(2 * time.Second): + t.Fatal("cmd did not return within timeout") + } +} + +func TestRunAttachTermination(t *testing.T) { + p, tty, err := pty.Open() + assert.NilError(t, err) defer func() { _ = tty.Close() _ = p.Close() }() + var conn net.Conn killCh := make(chan struct{}) attachCh := make(chan struct{}) fakeCLI := test.NewFakeCli(&fakeClient{ @@ -61,42 +134,143 @@ func TestRunAttachTermination(t *testing.T) { }, containerAttachFunc: func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) { server, client := net.Pipe() + conn = server t.Cleanup(func() { _ = server.Close() }) attachCh <- struct{}{} return types.NewHijackedResponse(client, types.MediaTypeRawStream), nil }, - Version: "1.36", + waitFunc: func(_ string) (<-chan container.WaitResponse, <-chan error) { + responseChan := make(chan container.WaitResponse, 1) + errChan := make(chan error) + + responseChan <- container.WaitResponse{ + StatusCode: 130, + } + return responseChan, errChan + }, + // use new (non-legacy) wait API + // see: 38591f20d07795aaef45d400df89ca12f29c603b + Version: "1.30", }, func(fc *test.FakeCli) { fc.SetOut(streams.NewOut(tty)) fc.SetIn(streams.NewIn(tty)) }) - ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM) - defer cancel() - - assert.Equal(t, fakeCLI.In().IsTerminal(), true) - assert.Equal(t, fakeCLI.Out().IsTerminal(), true) cmd := NewRunCommand(fakeCLI) cmd.SetArgs([]string{"-it", "busybox"}) cmd.SilenceUsage = true + cmdErrC := make(chan error, 1) go func() { - assert.ErrorIs(t, cmd.ExecuteContext(ctx), context.Canceled) + cmdErrC <- cmd.Execute() }() + // run command should attempt to attach to the container select { case <-time.After(5 * time.Second): - t.Fatal("containerAttachFunc was not called before the 5 second timeout") + t.Fatal("containerAttachFunc was not called before the timeout") case <-attachCh: } - assert.NilError(t, syscall.Kill(syscall.Getpid(), syscall.SIGTERM)) + assert.NilError(t, syscall.Kill(syscall.Getpid(), syscall.SIGINT)) + // end stream from "container" so that we'll detach + conn.Close() + select { - case <-time.After(5 * time.Second): - cancel() - t.Fatal("containerKillFunc was not called before the 5 second timeout") case <-killCh: + case <-time.After(5 * time.Second): + t.Fatal("containerKillFunc was not called before the timeout") + } + + select { + case cmdErr := <-cmdErrC: + assert.Equal(t, cmdErr, cli.StatusError{ + StatusCode: 130, + }) + case <-time.After(2 * time.Second): + t.Fatal("cmd did not return before the timeout") + } +} + +func TestRunPullTermination(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + attachCh := make(chan struct{}) + fakeCLI := test.NewFakeCli(&fakeClient{ + createContainerFunc: func(config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, + platform *specs.Platform, containerName string, + ) (container.CreateResponse, error) { + select { + case <-ctx.Done(): + return container.CreateResponse{}, ctx.Err() + default: + } + return container.CreateResponse{}, fakeNotFound{} + }, + containerAttachFunc: func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) { + return types.HijackedResponse{}, errors.New("shouldn't try to attach to a container") + }, + imageCreateFunc: func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) { + server, client := net.Pipe() + t.Cleanup(func() { + _ = server.Close() + }) + go func() { + enc := json.NewEncoder(server) + for i := 0; i < 100; i++ { + select { + case <-ctx.Done(): + assert.NilError(t, server.Close(), "failed to close imageCreateFunc server") + return + default: + } + assert.NilError(t, enc.Encode(jsonmessage.JSONMessage{ + Status: "Downloading", + ID: fmt.Sprintf("id-%d", i), + TimeNano: time.Now().UnixNano(), + Time: time.Now().Unix(), + Progress: &jsonmessage.JSONProgress{ + Current: int64(i), + Total: 100, + Start: 0, + }, + })) + time.Sleep(100 * time.Millisecond) + } + }() + attachCh <- struct{}{} + return client, nil + }, + Version: "1.30", + }) + + cmd := NewRunCommand(fakeCLI) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"foobar:latest"}) + + cmdErrC := make(chan error, 1) + go func() { + cmdErrC <- cmd.ExecuteContext(ctx) + }() + + select { + case <-time.After(5 * time.Second): + t.Fatal("imageCreateFunc was not called before the timeout") + case <-attachCh: + } + + cancel() + + select { + case cmdErr := <-cmdErrC: + assert.Equal(t, cmdErr, cli.StatusError{ + StatusCode: 125, + }) + case <-time.After(10 * time.Second): + t.Fatal("cmd did not return before the timeout") } } @@ -127,23 +301,27 @@ func TestRunCommandWithContentTrustErrors(t *testing.T) { }, } for _, tc := range testCases { - fakeCLI := test.NewFakeCli(&fakeClient{ - createContainerFunc: func(config *container.Config, - hostConfig *container.HostConfig, - networkingConfig *network.NetworkingConfig, - platform *specs.Platform, - containerName string, - ) (container.CreateResponse, error) { - return container.CreateResponse{}, errors.New("shouldn't try to pull image") - }, - }, test.EnableContentTrust) - fakeCLI.SetNotaryClient(tc.notaryFunc) - cmd := NewRunCommand(fakeCLI) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - err := cmd.Execute() - assert.Assert(t, err != nil) - assert.Assert(t, is.Contains(fakeCLI.ErrBuffer().String(), tc.expectedError)) + tc := tc + t.Run(tc.name, func(t *testing.T) { + fakeCLI := test.NewFakeCli(&fakeClient{ + createContainerFunc: func(config *container.Config, + hostConfig *container.HostConfig, + networkingConfig *network.NetworkingConfig, + platform *specs.Platform, + containerName string, + ) (container.CreateResponse, error) { + return container.CreateResponse{}, errors.New("shouldn't try to pull image") + }, + }, test.EnableContentTrust) + fakeCLI.SetNotaryClient(tc.notaryFunc) + cmd := NewRunCommand(fakeCLI) + cmd.SetArgs(tc.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + err := cmd.Execute() + assert.Assert(t, err != nil) + assert.Assert(t, is.Contains(fakeCLI.ErrBuffer().String(), tc.expectedError)) + }) } } diff --git a/cli/command/container/stats.go b/cli/command/container/stats.go index dd5e35d5fae8..d6afb0528c6f 100644 --- a/cli/command/container/stats.go +++ b/cli/command/container/stats.go @@ -1,6 +1,7 @@ package container import ( + "bytes" "context" "fmt" "io" @@ -264,31 +265,50 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) // so we unlikely hit this code in practice. 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{ - Output: dockerCLI.Out(), + Output: &statsTextBuffer, Format: NewStatsFormat(format, daemonOSType), } - cleanScreen := func() { - if !options.NoStream { - _, _ = fmt.Fprint(dockerCLI.Out(), "\033[2J") - _, _ = fmt.Fprint(dockerCLI.Out(), "\033[H") - } - } var err error ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() for range ticker.C { - cleanScreen() var ccStats []StatsEntry cStats.mu.RLock() for _, c := range cStats.cs { ccStats = append(ccStats, c.GetStatistics()) } cStats.mu.RUnlock() + + if !options.NoStream { + // Start by moving the cursor to the top-left + _, _ = fmt.Fprint(&statsTextBuffer, "\033[H") + } + if err = statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil { break } + + if !options.NoStream { + for _, line := range strings.Split(statsTextBuffer.String(), "\n") { + // In case the new text is shorter than the one we are writing over, + // we'll append the "erase line" escape sequence to clear the remaining text. + _, _ = fmt.Fprint(&statsTextBuffer, line, "\033[K\n") + } + + // We might have fewer containers than before, so let's clear the remaining text + _, _ = fmt.Fprint(&statsTextBuffer, "\033[J") + } + + _, _ = fmt.Fprint(dockerCLI.Out(), statsTextBuffer.String()) + statsTextBuffer.Reset() + if len(cStats.cs) == 0 && !showAll { break } diff --git a/cli/command/container/stop.go b/cli/command/container/stop.go index c221fb4448ac..c99a7c456dbc 100644 --- a/cli/command/container/stop.go +++ b/cli/command/container/stop.go @@ -43,6 +43,9 @@ func NewStopCommand(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container") flags.IntVarP(&opts.timeout, "time", "t", 0, "Seconds to wait before killing the container") + + _ = cmd.RegisterFlagCompletionFunc("signal", completeSignals) + return cmd } diff --git a/cli/command/container/update.go b/cli/command/container/update.go index 7ea668830b1b..5720f39bceaa 100644 --- a/cli/command/container/update.go +++ b/cli/command/container/update.go @@ -83,6 +83,8 @@ func NewUpdateCommand(dockerCli command.Cli) *cobra.Command { flags.Var(&options.cpus, "cpus", "Number of CPUs") flags.SetAnnotation("cpus", "version", []string{"1.29"}) + _ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies) + return cmd } diff --git a/cli/command/container/utils_test.go b/cli/command/container/utils_test.go index 7903113c566f..7d08b37844d2 100644 --- a/cli/command/container/utils_test.go +++ b/cli/command/container/utils_test.go @@ -38,7 +38,7 @@ func waitFn(cid string) (<-chan container.WaitResponse, <-chan error) { } func TestWaitExitOrRemoved(t *testing.T) { - testcases := []struct { + tests := []struct { cid string exitCode int }{ @@ -61,9 +61,11 @@ func TestWaitExitOrRemoved(t *testing.T) { } client := &fakeClient{waitFunc: waitFn, Version: api.DefaultVersion} - for _, testcase := range testcases { - statusC := waitExitOrRemoved(context.Background(), client, testcase.cid, true) - exitCode := <-statusC - assert.Check(t, is.Equal(testcase.exitCode, exitCode)) + for _, tc := range tests { + t.Run(tc.cid, func(t *testing.T) { + statusC := waitExitOrRemoved(context.Background(), client, tc.cid, true) + exitCode := <-statusC + assert.Check(t, is.Equal(tc.exitCode, exitCode)) + }) } } diff --git a/cli/command/context.go b/cli/command/context.go index af2298bb5070..404a6a13ea8b 100644 --- a/cli/command/context.go +++ b/cli/command/context.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package command diff --git a/cli/command/context/create.go b/cli/command/context/create.go index 3f2cefeabc96..a77151c25208 100644 --- a/cli/command/context/create.go +++ b/cli/command/context/create.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package context diff --git a/cli/command/context/create_test.go b/cli/command/context/create_test.go index 5d3f23fc3188..44ddfc6de080 100644 --- a/cli/command/context/create_test.go +++ b/cli/command/context/create_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package context diff --git a/cli/command/context/inspect.go b/cli/command/context/inspect.go index 7302a6e33dab..3794b417c457 100644 --- a/cli/command/context/inspect.go +++ b/cli/command/context/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package context diff --git a/cli/command/context/list.go b/cli/command/context/list.go index 83d48927aedb..23402e866495 100644 --- a/cli/command/context/list.go +++ b/cli/command/context/list.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package context diff --git a/cli/command/context_test.go b/cli/command/context_test.go index bf5ecf7dd39c..8bfd89b564f5 100644 --- a/cli/command/context_test.go +++ b/cli/command/context_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package command diff --git a/cli/command/defaultcontextstore.go b/cli/command/defaultcontextstore.go index 2582257680c3..c5b310e980ec 100644 --- a/cli/command/defaultcontextstore.go +++ b/cli/command/defaultcontextstore.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package command diff --git a/cli/command/defaultcontextstore_test.go b/cli/command/defaultcontextstore_test.go index 8f14f7e121bd..8fb8426e4858 100644 --- a/cli/command/defaultcontextstore_test.go +++ b/cli/command/defaultcontextstore_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package command diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go index aeaa03a6d3b1..10d4e4b4422f 100644 --- a/cli/command/formatter/container.go +++ b/cli/command/formatter/container.go @@ -1,10 +1,11 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package formatter import ( "fmt" + "net" "sort" "strconv" "strings" @@ -331,7 +332,8 @@ func DisplayablePorts(ports []types.Port) string { portKey := port.Type if port.IP != "" { if port.PublicPort != current { - hostMappings = append(hostMappings, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type)) + hAddrPort := net.JoinHostPort(port.IP, strconv.Itoa(int(port.PublicPort))) + hostMappings = append(hostMappings, fmt.Sprintf("%s->%d/%s", hAddrPort, port.PrivatePort, port.Type)) continue } portKey = port.IP + "/" + port.Type diff --git a/cli/command/formatter/container_test.go b/cli/command/formatter/container_test.go index 8f0d5e5c31d6..7f7632bdce1b 100644 --- a/cli/command/formatter/container_test.go +++ b/cli/command/formatter/container_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package formatter @@ -471,6 +471,16 @@ func TestDisplayablePorts(t *testing.T) { }, "0.0.0.0:0->9988/tcp", }, + { + []types.Port{ + { + IP: "::", + PrivatePort: 9988, + Type: "tcp", + }, + }, + "[::]:0->9988/tcp", + }, { []types.Port{ { diff --git a/cli/command/formatter/custom.go b/cli/command/formatter/custom.go index ee7d14830e30..043e268dbea9 100644 --- a/cli/command/formatter/custom.go +++ b/cli/command/formatter/custom.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package formatter diff --git a/cli/command/formatter/formatter.go b/cli/command/formatter/formatter.go index 4f1037725541..5873cce8d44d 100644 --- a/cli/command/formatter/formatter.go +++ b/cli/command/formatter/formatter.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package formatter diff --git a/cli/command/formatter/formatter_test.go b/cli/command/formatter/formatter_test.go index c0af96d3028b..7a95f2112ed9 100644 --- a/cli/command/formatter/formatter_test.go +++ b/cli/command/formatter/formatter_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package formatter diff --git a/cli/command/formatter/reflect.go b/cli/command/formatter/reflect.go index e2c0b28f53bd..fe8def610bbf 100644 --- a/cli/command/formatter/reflect.go +++ b/cli/command/formatter/reflect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package formatter diff --git a/cli/command/formatter/reflect_test.go b/cli/command/formatter/reflect_test.go index b68bad94ad8c..79737ec5f783 100644 --- a/cli/command/formatter/reflect_test.go +++ b/cli/command/formatter/reflect_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package formatter diff --git a/cli/command/formatter/volume_test.go b/cli/command/formatter/volume_test.go index 3a8869eadca7..a8ed971cefbc 100644 --- a/cli/command/formatter/volume_test.go +++ b/cli/command/formatter/volume_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package formatter diff --git a/cli/command/idresolver/idresolver.go b/cli/command/idresolver/idresolver.go index 802ff46fe18b..f73eb16e2cc2 100644 --- a/cli/command/idresolver/idresolver.go +++ b/cli/command/idresolver/idresolver.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package idresolver diff --git a/cli/command/image/build.go b/cli/command/image/build.go index 409b6cda18f1..4a258646dcce 100644 --- a/cli/command/image/build.go +++ b/cli/command/image/build.go @@ -15,8 +15,10 @@ import ( "strings" "github.com/distribution/reference" + "github.com/docker/cli-docs-tool/annotation" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/image/build" "github.com/docker/cli/opts" "github.com/docker/docker/api" @@ -104,7 +106,7 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command { }, Annotations: map[string]string{ "category-top": "4", - "aliases": "docker image build, docker build, docker buildx build, docker builder build", + "aliases": "docker image build, docker build, docker builder build", }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveFilterDirs @@ -114,9 +116,12 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.VarP(&options.tags, "tag", "t", `Name and optionally a tag in the "name:tag" format`) + flags.SetAnnotation("tag", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#tag"}) flags.Var(&options.buildArgs, "build-arg", "Set build-time variables") + flags.SetAnnotation("build-arg", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#build-arg"}) flags.Var(options.ulimits, "ulimit", "Ulimit options") flags.StringVarP(&options.dockerfileName, "file", "f", "", `Name of the Dockerfile (Default is "PATH/Dockerfile")`) + flags.SetAnnotation("file", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#file"}) flags.VarP(&options.memory, "memory", "m", "Memory limit") flags.Var(&options.memorySwap, "memory-swap", `Swap limit equal to memory plus swap: -1 to enable unlimited swap`) flags.Var(&options.shmSize, "shm-size", `Size of "/dev/shm"`) @@ -126,6 +131,7 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command { flags.StringVar(&options.cpuSetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") flags.StringVar(&options.cpuSetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") flags.StringVar(&options.cgroupParent, "cgroup-parent", "", `Set the parent cgroup for the "RUN" instructions during build`) + flags.SetAnnotation("cgroup-parent", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#cgroup-parent"}) flags.StringVar(&options.isolation, "isolation", "", "Container isolation technology") flags.Var(&options.labels, "label", "Set metadata for an image") flags.BoolVar(&options.noCache, "no-cache", false, "Do not use cache when building the image") @@ -138,8 +144,11 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command { flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options") flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build") flags.SetAnnotation("network", "version", []string{"1.25"}) + flags.SetAnnotation("network", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#network"}) flags.Var(&options.extraHosts, "add-host", `Add a custom host-to-IP mapping ("host:ip")`) + flags.SetAnnotation("add-host", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#add-host"}) flags.StringVar(&options.target, "target", "", "Set the target build stage to build.") + flags.SetAnnotation("target", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#target"}) flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file") command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled()) @@ -151,6 +160,8 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command { flags.SetAnnotation("squash", "experimental", nil) flags.SetAnnotation("squash", "version", []string{"1.25"}) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) + return cmd } diff --git a/cli/command/image/build_test.go b/cli/command/image/build_test.go index 1548261b3d46..b13ba3b0c1db 100644 --- a/cli/command/image/build_test.go +++ b/cli/command/image/build_test.go @@ -127,6 +127,7 @@ func TestRunBuildFromGitHubSpecialCase(t *testing.T) { // Clone a small repo that exists so git doesn't prompt for credentials cmd.SetArgs([]string{"github.com/docker/for-win"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) err := cmd.Execute() assert.ErrorContains(t, err, "unable to prepare context") assert.ErrorContains(t, err, "docker-build-git") diff --git a/cli/command/image/history.go b/cli/command/image/history.go index e36b9bc03cb8..1c0ae409811a 100644 --- a/cli/command/image/history.go +++ b/cli/command/image/history.go @@ -5,6 +5,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" "github.com/spf13/cobra" @@ -31,6 +32,7 @@ func NewHistoryCommand(dockerCli command.Cli) *cobra.Command { opts.image = args[0] return runHistory(cmd.Context(), dockerCli, opts) }, + ValidArgsFunction: completion.ImageNames(dockerCli), Annotations: map[string]string{ "aliases": "docker image history, docker history", }, diff --git a/cli/command/image/history_test.go b/cli/command/image/history_test.go index 4f3f54677b30..86eeaba3ba2e 100644 --- a/cli/command/image/history_test.go +++ b/cli/command/image/history_test.go @@ -35,10 +35,14 @@ func TestNewHistoryCommandErrors(t *testing.T) { }, } for _, tc := range testCases { - cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc})) - cmd.SetOut(io.Discard) - cmd.SetArgs(tc.args) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc})) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } diff --git a/cli/command/image/import.go b/cli/command/image/import.go index 9920b5e0cdc4..f6ad73d9baa4 100644 --- a/cli/command/image/import.go +++ b/cli/command/image/import.go @@ -7,6 +7,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/completion" dockeropts "github.com/docker/cli/opts" "github.com/docker/docker/api/types/image" "github.com/docker/docker/pkg/jsonmessage" @@ -47,6 +48,7 @@ func NewImportCommand(dockerCli command.Cli) *cobra.Command { flags.VarP(&options.changes, "change", "c", "Apply Dockerfile instruction to the created image") flags.StringVarP(&options.message, "message", "m", "", "Set commit message for imported image") command.AddPlatformFlag(flags, &options.platform) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) return cmd } diff --git a/cli/command/image/import_test.go b/cli/command/image/import_test.go index 7db6eea4f0dd..59a6ced04d40 100644 --- a/cli/command/image/import_test.go +++ b/cli/command/image/import_test.go @@ -36,6 +36,7 @@ func TestNewImportCommandErrors(t *testing.T) { for _, tc := range testCases { cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc})) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } @@ -44,6 +45,7 @@ func TestNewImportCommandErrors(t *testing.T) { func TestNewImportCommandInvalidFile(t *testing.T) { cmd := NewImportCommand(test.NewFakeCli(&fakeClient{})) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) cmd.SetArgs([]string{"testdata/import-command-success.unexistent-file"}) assert.ErrorContains(t, cmd.Execute(), "testdata/import-command-success.unexistent-file") } @@ -96,9 +98,13 @@ func TestNewImportCommandSuccess(t *testing.T) { }, } for _, tc := range testCases { - cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc})) - cmd.SetOut(io.Discard) - cmd.SetArgs(tc.args) - assert.NilError(t, cmd.Execute()) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc})) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + assert.NilError(t, cmd.Execute()) + }) } } diff --git a/cli/command/image/inspect.go b/cli/command/image/inspect.go index 96b3bb8f4cbd..b0e4fba52735 100644 --- a/cli/command/image/inspect.go +++ b/cli/command/image/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package image @@ -8,6 +8,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/inspect" flagsHelper "github.com/docker/cli/cli/flags" "github.com/spf13/cobra" @@ -30,6 +31,7 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { opts.refs = args return runInspect(cmd.Context(), dockerCli, opts) }, + ValidArgsFunction: completion.ImageNames(dockerCli), } flags := cmd.Flags() diff --git a/cli/command/image/inspect_test.go b/cli/command/image/inspect_test.go index fd0067e022f7..f98aaf049830 100644 --- a/cli/command/image/inspect_test.go +++ b/cli/command/image/inspect_test.go @@ -25,10 +25,14 @@ func TestNewInspectCommandErrors(t *testing.T) { }, } for _, tc := range testCases { - cmd := newInspectCommand(test.NewFakeCli(&fakeClient{})) - cmd.SetOut(io.Discard) - cmd.SetArgs(tc.args) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cmd := newInspectCommand(test.NewFakeCli(&fakeClient{})) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } diff --git a/cli/command/image/list.go b/cli/command/image/list.go index a691efed453b..cfe73c5adeb3 100644 --- a/cli/command/image/list.go +++ b/cli/command/image/list.go @@ -2,6 +2,7 @@ package image import ( "context" + "errors" "fmt" "io" @@ -24,6 +25,7 @@ type imagesOptions struct { format string filter opts.FilterOpt calledAs string + tree bool } // NewImagesCommand creates a new `docker images` command @@ -59,6 +61,10 @@ func NewImagesCommand(dockerCLI command.Cli) *cobra.Command { flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp) flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided") + flags.BoolVar(&options.tree, "tree", false, "List multi-platform images as a tree (EXPERIMENTAL)") + flags.SetAnnotation("tree", "version", []string{"1.47"}) + flags.SetAnnotation("tree", "experimentalCLI", nil) + return cmd } @@ -75,6 +81,26 @@ func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions filters.Add("reference", options.matchName) } + if options.tree { + if options.quiet { + return errors.New("--quiet is not yet supported with --tree") + } + if options.noTrunc { + return errors.New("--no-trunc is not yet supported with --tree") + } + if options.showDigests { + return errors.New("--show-digest is not yet supported with --tree") + } + if options.format != "" { + return errors.New("--format is not yet supported with --tree") + } + + return runTree(ctx, dockerCLI, treeOptions{ + all: options.all, + filters: filters, + }) + } + images, err := dockerCLI.Client().ImageList(ctx, image.ListOptions{ All: options.all, Filters: filters, diff --git a/cli/command/image/list_test.go b/cli/command/image/list_test.go index 2b5259b1f53d..4917c539398f 100644 --- a/cli/command/image/list_test.go +++ b/cli/command/image/list_test.go @@ -35,10 +35,14 @@ func TestNewImagesCommandErrors(t *testing.T) { }, } for _, tc := range testCases { - cmd := NewImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc})) - cmd.SetOut(io.Discard) - cmd.SetArgs(tc.args) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cmd := NewImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc})) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } @@ -79,14 +83,18 @@ func TestNewImagesCommandSuccess(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}) - cli.SetConfigFile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat}) - cmd := NewImagesCommand(cli) - cmd.SetOut(io.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.NilError(t, err) - golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("list-command-success.%s.golden", tc.name)) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}) + cli.SetConfigFile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat}) + cmd := NewImagesCommand(cli) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + err := cmd.Execute() + assert.NilError(t, err) + golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("list-command-success.%s.golden", tc.name)) + }) } } diff --git a/cli/command/image/load_test.go b/cli/command/image/load_test.go index 8f1b46016b68..9c36ce32353b 100644 --- a/cli/command/image/load_test.go +++ b/cli/command/image/load_test.go @@ -40,12 +40,16 @@ func TestNewLoadCommandErrors(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}) - cli.In().SetIsTerminal(tc.isTerminalIn) - cmd := NewLoadCommand(cli) - cmd.SetOut(io.Discard) - cmd.SetArgs(tc.args) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}) + cli.In().SetIsTerminal(tc.isTerminalIn) + cmd := NewLoadCommand(cli) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } @@ -53,6 +57,7 @@ func TestNewLoadCommandInvalidInput(t *testing.T) { expectedError := "open *" cmd := NewLoadCommand(test.NewFakeCli(&fakeClient{})) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) cmd.SetArgs([]string{"--input", "*"}) err := cmd.Execute() assert.ErrorContains(t, err, expectedError) @@ -89,12 +94,15 @@ func TestNewLoadCommandSuccess(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}) - cmd := NewLoadCommand(cli) - cmd.SetOut(io.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.NilError(t, err) - golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("load-command-success.%s.golden", tc.name)) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}) + cmd := NewLoadCommand(cli) + cmd.SetOut(io.Discard) + cmd.SetArgs(tc.args) + err := cmd.Execute() + assert.NilError(t, err) + golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("load-command-success.%s.golden", tc.name)) + }) } } diff --git a/cli/command/image/prune_test.go b/cli/command/image/prune_test.go index e1df3cafe02c..1d6b2de983ad 100644 --- a/cli/command/image/prune_test.go +++ b/cli/command/image/prune_test.go @@ -39,12 +39,16 @@ func TestNewPruneCommandErrors(t *testing.T) { }, } for _, tc := range testCases { - cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{ - imagesPruneFunc: tc.imagesPruneFunc, - })) - cmd.SetOut(io.Discard) - cmd.SetArgs(tc.args) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{ + imagesPruneFunc: tc.imagesPruneFunc, + })) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } @@ -94,6 +98,7 @@ func TestNewPruneCommandSuccess(t *testing.T) { }, } for _, tc := range testCases { + tc := tc t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc}) // when prompted, answer "Y" to confirm the prune. @@ -119,5 +124,8 @@ func TestPrunePromptTermination(t *testing.T) { }, }) cmd := NewPruneCommand(cli) + cmd.SetArgs([]string{}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) test.TerminatePrompt(ctx, t, cmd, cli) } diff --git a/cli/command/image/pull.go b/cli/command/image/pull.go index 9ec9771e2885..93388253f75a 100644 --- a/cli/command/image/pull.go +++ b/cli/command/image/pull.go @@ -50,6 +50,8 @@ func NewPullCommand(dockerCli command.Cli) *cobra.Command { command.AddPlatformFlag(flags, &opts.platform) command.AddTrustVerificationFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled()) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) + return cmd } diff --git a/cli/command/image/pull_test.go b/cli/command/image/pull_test.go index 3aa6e5524409..43bc772ea75b 100644 --- a/cli/command/image/pull_test.go +++ b/cli/command/image/pull_test.go @@ -38,11 +38,15 @@ func TestNewPullCommandErrors(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{}) - cmd := NewPullCommand(cli) - cmd.SetOut(io.Discard) - cmd.SetArgs(tc.args) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{}) + cmd := NewPullCommand(cli) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } @@ -69,18 +73,22 @@ func TestNewPullCommandSuccess(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{ - imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) { - assert.Check(t, is.Equal(tc.expectedTag, ref), tc.name) - return io.NopCloser(strings.NewReader("")), nil - }, + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) { + assert.Check(t, is.Equal(tc.expectedTag, ref), tc.name) + return io.NopCloser(strings.NewReader("")), nil + }, + }) + cmd := NewPullCommand(cli) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + err := cmd.Execute() + assert.NilError(t, err) + golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("pull-command-success.%s.golden", tc.name)) }) - cmd := NewPullCommand(cli) - cmd.SetOut(io.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.NilError(t, err) - golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("pull-command-success.%s.golden", tc.name)) } } @@ -111,16 +119,20 @@ func TestNewPullCommandWithContentTrustErrors(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{ - imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) { - return io.NopCloser(strings.NewReader("")), errors.New("shouldn't try to pull image") - }, - }, test.EnableContentTrust) - cli.SetNotaryClient(tc.notaryFunc) - cmd := NewPullCommand(cli) - cmd.SetOut(io.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.ErrorContains(t, err, tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader("")), errors.New("shouldn't try to pull image") + }, + }, test.EnableContentTrust) + cli.SetNotaryClient(tc.notaryFunc) + cmd := NewPullCommand(cli) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + err := cmd.Execute() + assert.ErrorContains(t, err, tc.expectedError) + }) } } diff --git a/cli/command/image/push.go b/cli/command/image/push.go index 6abacbf71d3b..9ad2abd1ff87 100644 --- a/cli/command/image/push.go +++ b/cli/command/image/push.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package image @@ -8,7 +8,7 @@ import ( "encoding/json" "fmt" "io" - "os" + "strings" "github.com/containerd/platforms" "github.com/distribution/reference" @@ -58,11 +58,18 @@ func NewPushCommand(dockerCli command.Cli) *cobra.Command { flags.BoolVarP(&opts.all, "all-tags", "a", false, "Push all tags of an image to the repository") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress verbose output") command.AddTrustSigningFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled()) - flags.StringVar(&opts.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), + + // Don't default to DOCKER_DEFAULT_PLATFORM env variable, always default to + // pushing the image as-is. This also avoids forcing the platform selection + // on older APIs which don't support it. + flags.StringVar(&opts.platform, "platform", "", `Push a platform-specific manifest as a single-platform image to the registry. +Image index won't be pushed, meaning that other manifests, including attestations won't be preserved. 'os[/arch[/variant]]': Explicit platform (eg. linux/amd64)`) flags.SetAnnotation("platform", "version", []string{"1.46"}) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) + return cmd } @@ -79,9 +86,9 @@ func RunPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error } platform = &p - printNote(dockerCli, `Selecting a single platform will only push one matching image manifest from a multi-platform image index. -This means that any other components attached to the multi-platform image index (like Buildkit attestations) won't be pushed. -If you want to only push a single platform image while preserving the attestations, please use 'docker convert\n' + printNote(dockerCli, `Using --platform pushes only the specified platform manifest of a multi-platform image index. +Other components, like attestations, will not be included. +To push the complete multi-platform image, remove the --platform flag. `) } @@ -179,9 +186,22 @@ func handleAux(dockerCli command.Cli) func(jm jsonmessage.JSONMessage) { func printNote(dockerCli command.Cli, format string, args ...any) { if dockerCli.Err().IsTerminal() { - _, _ = fmt.Fprint(dockerCli.Err(), aec.WhiteF.Apply(aec.CyanB.Apply("[ NOTE ]"))+" ") - } else { - _, _ = fmt.Fprint(dockerCli.Err(), "[ NOTE ] ") + format = strings.ReplaceAll(format, "--platform", aec.Bold.Apply("--platform")) + } + + header := " Info -> " + padding := len(header) + if dockerCli.Err().IsTerminal() { + padding = len("i Info > ") + header = aec.Bold.Apply(aec.LightCyanB.Apply(aec.BlackF.Apply("i")) + " " + aec.LightCyanF.Apply("Info → ")) + } + + _, _ = fmt.Fprint(dockerCli.Err(), header) + s := fmt.Sprintf(format, args...) + for idx, line := range strings.Split(s, "\n") { + if idx > 0 { + _, _ = fmt.Fprint(dockerCli.Err(), strings.Repeat(" ", padding)) + } + _, _ = fmt.Fprintln(dockerCli.Err(), aec.Italic.Apply(line)) } - _, _ = fmt.Fprintf(dockerCli.Err(), aec.Bold.Apply(format)+"\n", args...) } diff --git a/cli/command/image/push_test.go b/cli/command/image/push_test.go index 6ba7235775ce..a659c6c510cc 100644 --- a/cli/command/image/push_test.go +++ b/cli/command/image/push_test.go @@ -38,11 +38,15 @@ func TestNewPushCommandErrors(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc}) - cmd := NewPushCommand(cli) - cmd.SetOut(io.Discard) - cmd.SetArgs(tc.args) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc}) + cmd := NewPushCommand(cli) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } @@ -73,6 +77,7 @@ func TestNewPushCommandSuccess(t *testing.T) { }) cmd := NewPushCommand(cli) cmd.SetOut(cli.OutBuffer()) + cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) assert.NilError(t, cmd.Execute()) if tc.output != "" { diff --git a/cli/command/image/remove.go b/cli/command/image/remove.go index e0983f377f9b..3fb0af5c03de 100644 --- a/cli/command/image/remove.go +++ b/cli/command/image/remove.go @@ -7,6 +7,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/completion" "github.com/docker/docker/api/types/image" "github.com/docker/docker/errdefs" "github.com/pkg/errors" @@ -29,6 +30,7 @@ func NewRemoveCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runRemove(cmd.Context(), dockerCli, opts, args) }, + ValidArgsFunction: completion.ImageNames(dockerCli), Annotations: map[string]string{ "aliases": "docker image rm, docker image remove, docker rmi", }, diff --git a/cli/command/image/remove_test.go b/cli/command/image/remove_test.go index b1035da92445..d4340b381914 100644 --- a/cli/command/image/remove_test.go +++ b/cli/command/image/remove_test.go @@ -62,11 +62,13 @@ func TestNewRemoveCommandErrors(t *testing.T) { }, } for _, tc := range testCases { + tc := tc t.Run(tc.name, func(t *testing.T) { cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{ imageRemoveFunc: tc.imageRemoveFunc, })) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) }) @@ -119,10 +121,12 @@ func TestNewRemoveCommandSuccess(t *testing.T) { }, } for _, tc := range testCases { + tc := tc t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc}) cmd := NewRemoveCommand(cli) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) assert.NilError(t, cmd.Execute()) assert.Check(t, is.Equal(tc.expectedStderr, cli.ErrBuffer().String())) diff --git a/cli/command/image/save_test.go b/cli/command/image/save_test.go index 22434c736fd3..c6cff4cc3c22 100644 --- a/cli/command/image/save_test.go +++ b/cli/command/image/save_test.go @@ -52,12 +52,16 @@ func TestNewSaveCommandErrors(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc}) - cli.Out().SetIsTerminal(tc.isTerminal) - cmd := NewSaveCommand(cli) - cmd.SetOut(io.Discard) - cmd.SetArgs(tc.args) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc}) + cli.Out().SetIsTerminal(tc.isTerminal) + cmd := NewSaveCommand(cli) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } @@ -77,7 +81,7 @@ func TestNewSaveCommandSuccess(t *testing.T) { return io.NopCloser(strings.NewReader("")), nil }, deferredFunc: func() { - os.Remove("save_tmp_file") + _ = os.Remove("save_tmp_file") }, }, { @@ -92,16 +96,20 @@ func TestNewSaveCommandSuccess(t *testing.T) { }, } for _, tc := range testCases { - cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{ - imageSaveFunc: func(images []string) (io.ReadCloser, error) { - return io.NopCloser(strings.NewReader("")), nil - }, - })) - cmd.SetOut(io.Discard) - cmd.SetArgs(tc.args) - assert.NilError(t, cmd.Execute()) - if tc.deferredFunc != nil { - tc.deferredFunc() - } + tc := tc + t.Run(strings.Join(tc.args, " "), func(t *testing.T) { + cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{ + imageSaveFunc: func(images []string) (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader("")), nil + }, + })) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + assert.NilError(t, cmd.Execute()) + if tc.deferredFunc != nil { + tc.deferredFunc() + } + }) } } diff --git a/cli/command/image/tag_test.go b/cli/command/image/tag_test.go index 87e7aeb1e585..77bf81c2db6f 100644 --- a/cli/command/image/tag_test.go +++ b/cli/command/image/tag_test.go @@ -20,6 +20,7 @@ func TestCliNewTagCommandErrors(t *testing.T) { cmd := NewTagCommand(test.NewFakeCli(&fakeClient{})) cmd.SetArgs(args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), expectedError) } } diff --git a/cli/command/image/tree.go b/cli/command/image/tree.go new file mode 100644 index 000000000000..ec4ef7b4e4b4 --- /dev/null +++ b/cli/command/image/tree.go @@ -0,0 +1,393 @@ +package image + +import ( + "context" + "fmt" + "sort" + "strings" + "unicode/utf8" + + "github.com/containerd/platforms" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/streams" + "github.com/docker/docker/api/types/filters" + imagetypes "github.com/docker/docker/api/types/image" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/go-units" + "github.com/morikuni/aec" +) + +type treeOptions struct { + all bool + filters filters.Args +} + +type treeView struct { + images []topImage + + // imageSpacing indicates whether there should be extra spacing between images. + imageSpacing bool +} + +func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error { + images, err := dockerCLI.Client().ImageList(ctx, imagetypes.ListOptions{ + All: opts.all, + Filters: opts.filters, + Manifests: true, + }) + if err != nil { + return err + } + + view := treeView{ + images: make([]topImage, 0, len(images)), + } + for _, img := range images { + details := imageDetails{ + ID: img.ID, + DiskUsage: units.HumanSizeWithPrecision(float64(img.Size), 3), + InUse: img.Containers > 0, + } + + var totalContent int64 + children := make([]subImage, 0, len(img.Manifests)) + for _, im := range img.Manifests { + if im.Kind != imagetypes.ManifestKindImage { + continue + } + + im := im + sub := subImage{ + Platform: platforms.Format(im.ImageData.Platform), + Available: im.Available, + Details: imageDetails{ + ID: im.ID, + DiskUsage: units.HumanSizeWithPrecision(float64(im.Size.Total), 3), + InUse: len(im.ImageData.Containers) > 0, + ContentSize: units.HumanSizeWithPrecision(float64(im.Size.Content), 3), + }, + } + + if sub.Details.InUse { + // Mark top-level parent image as used if any of its subimages are used. + details.InUse = true + } + + totalContent += im.Size.Content + children = append(children, sub) + + // Add extra spacing between images if there's at least one entry with children. + view.imageSpacing = true + } + + details.ContentSize = units.HumanSizeWithPrecision(float64(totalContent), 3) + + view.images = append(view.images, topImage{ + Names: img.RepoTags, + Details: details, + Children: children, + created: img.Created, + }) + } + + sort.Slice(view.images, func(i, j int) bool { + return view.images[i].created > view.images[j].created + }) + + return printImageTree(dockerCLI, view) +} + +type imageDetails struct { + ID string + DiskUsage string + InUse bool + ContentSize string +} + +type topImage struct { + Names []string + Details imageDetails + Children []subImage + + created int64 +} + +type subImage struct { + Platform string + Available bool + Details imageDetails +} + +const columnSpacing = 3 + +func printImageTree(dockerCLI command.Cli, view treeView) error { + out := dockerCLI.Out() + _, width := out.GetTtySize() + if width == 0 { + width = 80 + } + if width < 20 { + width = 20 + } + + warningColor := aec.LightYellowF + headerColor := aec.NewBuilder(aec.DefaultF, aec.Bold).ANSI + topNameColor := aec.NewBuilder(aec.BlueF, aec.Bold).ANSI + normalColor := aec.NewBuilder(aec.DefaultF).ANSI + greenColor := aec.NewBuilder(aec.GreenF).ANSI + untaggedColor := aec.NewBuilder(aec.Faint).ANSI + if !out.IsTerminal() { + headerColor = noColor{} + topNameColor = noColor{} + normalColor = noColor{} + greenColor = noColor{} + warningColor = noColor{} + untaggedColor = noColor{} + } + + _, _ = fmt.Fprintln(out, warningColor.Apply("WARNING: This is an experimental feature. The output may change and shouldn't be depended on.")) + _, _ = fmt.Fprintln(out, "") + + columns := []imgColumn{ + { + Title: "Image", + Align: alignLeft, + Width: 0, + }, + { + Title: "ID", + Align: alignLeft, + Width: 12, + DetailsValue: func(d *imageDetails) string { + return stringid.TruncateID(d.ID) + }, + }, + { + Title: "Disk usage", + Align: alignRight, + Width: 10, + DetailsValue: func(d *imageDetails) string { + return d.DiskUsage + }, + }, + { + Title: "Content size", + Align: alignRight, + Width: 12, + DetailsValue: func(d *imageDetails) string { + return d.ContentSize + }, + }, + { + Title: "In Use", + Align: alignCenter, + Width: 6, + Color: &greenColor, + DetailsValue: func(d *imageDetails) string { + if d.InUse { + return "✔" + } + return " " + }, + }, + } + + nameWidth := int(width) + for idx, h := range columns { + if h.Width == 0 { + continue + } + d := h.Width + if idx > 0 { + d += columnSpacing + } + // If the first column gets too short, remove remaining columns + if nameWidth-d < 12 { + columns = columns[:idx] + break + } + nameWidth -= d + } + + images := view.images + // Try to make the first column as narrow as possible + widest := widestFirstColumnValue(columns, images) + if nameWidth > widest { + nameWidth = widest + } + columns[0].Width = nameWidth + + // Print columns + for i, h := range columns { + if i > 0 { + _, _ = fmt.Fprint(out, strings.Repeat(" ", columnSpacing)) + } + + _, _ = fmt.Fprint(out, h.Print(headerColor, strings.ToUpper(h.Title))) + } + + _, _ = fmt.Fprintln(out) + + // Print images + for _, img := range images { + printNames(out, columns, img, topNameColor, untaggedColor) + printDetails(out, columns, normalColor, img.Details) + + if len(img.Children) > 0 || view.imageSpacing { + _, _ = fmt.Fprintln(out) + } + printChildren(out, columns, img, normalColor) + _, _ = fmt.Fprintln(out) + } + + return nil +} + +func printDetails(out *streams.Out, headers []imgColumn, defaultColor aec.ANSI, details imageDetails) { + for _, h := range headers { + if h.DetailsValue == nil { + continue + } + + _, _ = fmt.Fprint(out, strings.Repeat(" ", columnSpacing)) + clr := defaultColor + if h.Color != nil { + clr = *h.Color + } + val := h.DetailsValue(&details) + _, _ = fmt.Fprint(out, h.Print(clr, val)) + } +} + +func printChildren(out *streams.Out, headers []imgColumn, img topImage, normalColor aec.ANSI) { + for idx, sub := range img.Children { + clr := normalColor + if !sub.Available { + clr = normalColor.With(aec.Faint) + } + + if idx != len(img.Children)-1 { + _, _ = fmt.Fprint(out, headers[0].Print(clr, "├─ "+sub.Platform)) + } else { + _, _ = fmt.Fprint(out, headers[0].Print(clr, "└─ "+sub.Platform)) + } + + printDetails(out, headers, clr, sub.Details) + _, _ = fmt.Fprintln(out, "") + } +} + +func printNames(out *streams.Out, headers []imgColumn, img topImage, color, untaggedColor aec.ANSI) { + if len(img.Names) == 0 { + _, _ = fmt.Fprint(out, headers[0].Print(untaggedColor, "")) + } + + for nameIdx, name := range img.Names { + if nameIdx != 0 { + _, _ = fmt.Fprintln(out, "") + } + _, _ = fmt.Fprint(out, headers[0].Print(color, name)) + } +} + +type alignment int + +const ( + alignLeft alignment = iota + alignCenter + alignRight +) + +type imgColumn struct { + Title string + Width int + Align alignment + + DetailsValue func(*imageDetails) string + Color *aec.ANSI +} + +func truncateRunes(s string, length int) string { + runes := []rune(s) + if len(runes) > length { + return string(runes[:length-3]) + "..." + } + return s +} + +func (h imgColumn) Print(clr aec.ANSI, s string) string { + switch h.Align { + case alignCenter: + return h.PrintC(clr, s) + case alignRight: + return h.PrintR(clr, s) + case alignLeft: + } + return h.PrintL(clr, s) +} + +func (h imgColumn) PrintC(clr aec.ANSI, s string) string { + ln := utf8.RuneCountInString(s) + + if ln > h.Width { + return clr.Apply(truncateRunes(s, h.Width)) + } + + fill := h.Width - ln + + l := fill / 2 + r := fill - l + + return strings.Repeat(" ", l) + clr.Apply(s) + strings.Repeat(" ", r) +} + +func (h imgColumn) PrintL(clr aec.ANSI, s string) string { + ln := utf8.RuneCountInString(s) + if ln > h.Width { + return clr.Apply(truncateRunes(s, h.Width)) + } + + return clr.Apply(s) + strings.Repeat(" ", h.Width-ln) +} + +func (h imgColumn) PrintR(clr aec.ANSI, s string) string { + ln := utf8.RuneCountInString(s) + if ln > h.Width { + return clr.Apply(truncateRunes(s, h.Width)) + } + + return strings.Repeat(" ", h.Width-ln) + clr.Apply(s) +} + +type noColor struct{} + +func (a noColor) With(_ ...aec.ANSI) aec.ANSI { + return a +} + +func (a noColor) Apply(s string) string { + return s +} + +func (a noColor) String() string { + return "" +} + +// widestFirstColumnValue calculates the width needed to fully display the image names and platforms. +func widestFirstColumnValue(headers []imgColumn, images []topImage) int { + width := len(headers[0].Title) + for _, img := range images { + for _, name := range img.Names { + if len(name) > width { + width = len(name) + } + } + for _, sub := range img.Children { + pl := len(sub.Platform) + len("└─ ") + if pl > width { + width = pl + } + } + } + return width +} diff --git a/cli/command/inspect/inspector.go b/cli/command/inspect/inspector.go index d618e9214bbf..46ba8750d6a4 100644 --- a/cli/command/inspect/inspector.go +++ b/cli/command/inspect/inspector.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package inspect diff --git a/cli/command/manifest/annotate_test.go b/cli/command/manifest/annotate_test.go index 5e95d1c7f1f6..089b29c71600 100644 --- a/cli/command/manifest/annotate_test.go +++ b/cli/command/manifest/annotate_test.go @@ -35,6 +35,7 @@ func TestManifestAnnotateError(t *testing.T) { cmd := newAnnotateCommand(cli) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } @@ -52,6 +53,7 @@ func TestManifestAnnotate(t *testing.T) { cmd := newAnnotateCommand(cli) cmd.SetArgs([]string{"example.com/list:v1", "example.com/fake:0.0"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) expectedError := "manifest for image example.com/fake:0.0 does not exist" assert.ErrorContains(t, cmd.Execute(), expectedError) @@ -71,6 +73,7 @@ func TestManifestAnnotate(t *testing.T) { err = cmd.Flags().Set("verbose", "true") assert.NilError(t, err) cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"}) + cmd.SetErr(io.Discard) assert.NilError(t, cmd.Execute()) actual := cli.OutBuffer() expected := golden.Get(t, "inspect-annotate.golden") diff --git a/cli/command/manifest/cmd.go b/cli/command/manifest/cmd.go index a914ef6dbb4b..939f02b7bc5c 100644 --- a/cli/command/manifest/cmd.go +++ b/cli/command/manifest/cmd.go @@ -18,7 +18,7 @@ func NewManifestCommand(dockerCli command.Cli) *cobra.Command { Long: manifestDescription, Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + _, _ = fmt.Fprint(dockerCli.Err(), "\n"+cmd.UsageString()) }, Annotations: map[string]string{"experimentalCLI": ""}, } diff --git a/cli/command/manifest/create_test.go b/cli/command/manifest/create_test.go index 71fe6c63e9cd..0b1296e805a4 100644 --- a/cli/command/manifest/create_test.go +++ b/cli/command/manifest/create_test.go @@ -31,11 +31,15 @@ func TestManifestCreateErrors(t *testing.T) { } for _, tc := range testCases { - cli := test.NewFakeCli(nil) - cmd := newCreateListCommand(cli) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.expectedError, func(t *testing.T) { + cli := test.NewFakeCli(nil) + cmd := newCreateListCommand(cli) + cmd.SetArgs(tc.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } @@ -87,6 +91,7 @@ func TestManifestCreateRefuseAmend(t *testing.T) { cmd := newCreateListCommand(cli) cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) err = cmd.Execute() assert.Error(t, err, "refusing to amend an existing manifest list with no --amend flag") } @@ -109,6 +114,7 @@ func TestManifestCreateNoManifest(t *testing.T) { cmd := newCreateListCommand(cli) cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) err := cmd.Execute() assert.Error(t, err, "No such image: example.com/alpine:3.0") } diff --git a/cli/command/manifest/inspect_test.go b/cli/command/manifest/inspect_test.go index af4e17af3c97..0009b4177066 100644 --- a/cli/command/manifest/inspect_test.go +++ b/cli/command/manifest/inspect_test.go @@ -70,6 +70,7 @@ func TestInspectCommandLocalManifestNotFound(t *testing.T) { cmd := newInspectCommand(cli) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"}) err := cmd.Execute() assert.Error(t, err, "No such manifest: example.com/alpine:3.0") @@ -91,6 +92,7 @@ func TestInspectCommandNotFound(t *testing.T) { cmd := newInspectCommand(cli) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) cmd.SetArgs([]string{"example.com/alpine:3.0"}) err := cmd.Execute() assert.Error(t, err, "No such manifest: example.com/alpine:3.0") diff --git a/cli/command/manifest/push_test.go b/cli/command/manifest/push_test.go index 2276cf7e43f5..2a11fceda2cf 100644 --- a/cli/command/manifest/push_test.go +++ b/cli/command/manifest/push_test.go @@ -44,6 +44,7 @@ func TestManifestPushErrors(t *testing.T) { cmd := newPushListCommand(cli) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/manifest/rm_test.go b/cli/command/manifest/rm_test.go index b360c0d8bbfa..8d285c28c82a 100644 --- a/cli/command/manifest/rm_test.go +++ b/cli/command/manifest/rm_test.go @@ -56,6 +56,7 @@ func TestRmManifestNotCreated(t *testing.T) { cmd := newRmManifestListCommand(cli) cmd.SetArgs([]string{"example.com/first:1", "example.com/second:2"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) err = cmd.Execute() assert.Error(t, err, "No such manifest: example.com/first:1") diff --git a/cli/command/network/connect_test.go b/cli/command/network/connect_test.go index 89c9b8e607b2..cfdff2880605 100644 --- a/cli/command/network/connect_test.go +++ b/cli/command/network/connect_test.go @@ -38,6 +38,7 @@ func TestNetworkConnectErrors(t *testing.T) { ) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/network/create_test.go b/cli/command/network/create_test.go index 30707c2792e8..8eab2ff0bd81 100644 --- a/cli/command/network/create_test.go +++ b/cli/command/network/create_test.go @@ -137,6 +137,7 @@ func TestNetworkCreateErrors(t *testing.T) { assert.NilError(t, cmd.Flags().Set(key, value)) } cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/network/disconnect_test.go b/cli/command/network/disconnect_test.go index e60ecd6ac089..bcbb491d096a 100644 --- a/cli/command/network/disconnect_test.go +++ b/cli/command/network/disconnect_test.go @@ -36,6 +36,7 @@ func TestNetworkDisconnectErrors(t *testing.T) { ) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/network/formatter_test.go b/cli/command/network/formatter_test.go index 8a87a6760b04..a689efa635a2 100644 --- a/cli/command/network/formatter_test.go +++ b/cli/command/network/formatter_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package network diff --git a/cli/command/network/inspect.go b/cli/command/network/inspect.go index 200134befc0b..f00f94bba899 100644 --- a/cli/command/network/inspect.go +++ b/cli/command/network/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package network diff --git a/cli/command/network/list_test.go b/cli/command/network/list_test.go index 7b1ae329bc20..e92c0d09fa3e 100644 --- a/cli/command/network/list_test.go +++ b/cli/command/network/list_test.go @@ -36,6 +36,7 @@ func TestNetworkListErrors(t *testing.T) { }), ) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } @@ -82,6 +83,7 @@ func TestNetworkList(t *testing.T) { } for _, tc := range testCases { + tc := tc t.Run(tc.doc, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{networkListFunc: tc.networkListFunc}) cmd := newListCommand(cli) diff --git a/cli/command/network/prune_test.go b/cli/command/network/prune_test.go index b59dfff81419..16010d3c7420 100644 --- a/cli/command/network/prune_test.go +++ b/cli/command/network/prune_test.go @@ -2,6 +2,7 @@ package network import ( "context" + "io" "testing" "github.com/docker/cli/internal/test" @@ -20,5 +21,8 @@ func TestNetworkPrunePromptTermination(t *testing.T) { }, }) cmd := NewPruneCommand(cli) + cmd.SetArgs([]string{}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) test.TerminatePrompt(ctx, t, cmd, cli) } diff --git a/cli/command/network/remove_test.go b/cli/command/network/remove_test.go index 283f9544bd6c..05ec10b1ed56 100644 --- a/cli/command/network/remove_test.go +++ b/cli/command/network/remove_test.go @@ -114,5 +114,7 @@ func TestNetworkRemovePromptTermination(t *testing.T) { }) cmd := newRemoveCommand(cli) cmd.SetArgs([]string{"existing-network"}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) test.TerminatePrompt(ctx, t, cmd, cli) } diff --git a/cli/command/node/demote_test.go b/cli/command/node/demote_test.go index 64a11c098593..ad7590533d9c 100644 --- a/cli/command/node/demote_test.go +++ b/cli/command/node/demote_test.go @@ -44,6 +44,7 @@ func TestNodeDemoteErrors(t *testing.T) { })) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/node/formatter_test.go b/cli/command/node/formatter_test.go index e86d15a760cb..1dd659ca6c76 100644 --- a/cli/command/node/formatter_test.go +++ b/cli/command/node/formatter_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package node diff --git a/cli/command/node/inspect.go b/cli/command/node/inspect.go index 3ad98ef8b167..270a14bd2bdd 100644 --- a/cli/command/node/inspect.go +++ b/cli/command/node/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package node diff --git a/cli/command/node/inspect_test.go b/cli/command/node/inspect_test.go index 579330ad0828..9cb742fbb931 100644 --- a/cli/command/node/inspect_test.go +++ b/cli/command/node/inspect_test.go @@ -74,6 +74,7 @@ func TestNodeInspectErrors(t *testing.T) { assert.Check(t, cmd.Flags().Set(key, value)) } cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } @@ -105,13 +106,16 @@ func TestNodeInspectPretty(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{ - nodeInspectFunc: tc.nodeInspectFunc, + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + nodeInspectFunc: tc.nodeInspectFunc, + }) + cmd := newInspectCommand(cli) + cmd.SetArgs([]string{"nodeID"}) + assert.Check(t, cmd.Flags().Set("pretty", "true")) + assert.NilError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("node-inspect-pretty.%s.golden", tc.name)) }) - cmd := newInspectCommand(cli) - cmd.SetArgs([]string{"nodeID"}) - assert.Check(t, cmd.Flags().Set("pretty", "true")) - assert.NilError(t, cmd.Execute()) - golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("node-inspect-pretty.%s.golden", tc.name)) } } diff --git a/cli/command/node/list_test.go b/cli/command/node/list_test.go index 715e43cc8357..0ccf8a2d4e2a 100644 --- a/cli/command/node/list_test.go +++ b/cli/command/node/list_test.go @@ -48,6 +48,7 @@ func TestNodeListErrorOnAPIFailure(t *testing.T) { }) cmd := newListCommand(cli) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.Error(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/node/promote_test.go b/cli/command/node/promote_test.go index 42b3a7d755ed..bf4dce53959a 100644 --- a/cli/command/node/promote_test.go +++ b/cli/command/node/promote_test.go @@ -44,6 +44,7 @@ func TestNodePromoteErrors(t *testing.T) { })) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/node/ps_test.go b/cli/command/node/ps_test.go index 98318903b527..84b1612170b0 100644 --- a/cli/command/node/ps_test.go +++ b/cli/command/node/ps_test.go @@ -61,6 +61,7 @@ func TestNodePsErrors(t *testing.T) { assert.Check(t, cmd.Flags().Set(key, value)) } cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.Error(t, cmd.Execute(), tc.expectedError) } } @@ -133,19 +134,22 @@ func TestNodePs(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{ - infoFunc: tc.infoFunc, - nodeInspectFunc: tc.nodeInspectFunc, - taskInspectFunc: tc.taskInspectFunc, - taskListFunc: tc.taskListFunc, - serviceInspectFunc: tc.serviceInspectFunc, + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + infoFunc: tc.infoFunc, + nodeInspectFunc: tc.nodeInspectFunc, + taskInspectFunc: tc.taskInspectFunc, + taskListFunc: tc.taskListFunc, + serviceInspectFunc: tc.serviceInspectFunc, + }) + cmd := newPsCommand(cli) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + assert.Check(t, cmd.Flags().Set(key, value)) + } + assert.NilError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("node-ps.%s.golden", tc.name)) }) - cmd := newPsCommand(cli) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - assert.Check(t, cmd.Flags().Set(key, value)) - } - assert.NilError(t, cmd.Execute()) - golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("node-ps.%s.golden", tc.name)) } } diff --git a/cli/command/node/remove_test.go b/cli/command/node/remove_test.go index db7bbcc7de49..44b6342826f8 100644 --- a/cli/command/node/remove_test.go +++ b/cli/command/node/remove_test.go @@ -33,6 +33,7 @@ func TestNodeRemoveErrors(t *testing.T) { })) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/node/update_test.go b/cli/command/node/update_test.go index 0421e4757022..09881d7ec200 100644 --- a/cli/command/node/update_test.go +++ b/cli/command/node/update_test.go @@ -64,6 +64,7 @@ func TestNodeUpdateErrors(t *testing.T) { assert.Check(t, cmd.Flags().Set(key, value)) } cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/plugin/create_test.go b/cli/command/plugin/create_test.go index e05eb6085538..0472f6eae153 100644 --- a/cli/command/plugin/create_test.go +++ b/cli/command/plugin/create_test.go @@ -41,6 +41,7 @@ func TestCreateErrors(t *testing.T) { cmd := newCreateCommand(cli) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } @@ -53,6 +54,7 @@ func TestCreateErrorOnFileAsContextDir(t *testing.T) { cmd := newCreateCommand(cli) cmd.SetArgs([]string{"plugin-foo", tmpFile.Path()}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), "context must be a directory") } @@ -64,6 +66,7 @@ func TestCreateErrorOnContextDirWithoutConfig(t *testing.T) { cmd := newCreateCommand(cli) cmd.SetArgs([]string{"plugin-foo", tmpDir.Path()}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) expectedErr := "config.json: no such file or directory" if runtime.GOOS == "windows" { @@ -82,6 +85,7 @@ func TestCreateErrorOnInvalidConfig(t *testing.T) { cmd := newCreateCommand(cli) cmd.SetArgs([]string{"plugin-foo", tmpDir.Path()}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), "invalid") } @@ -98,6 +102,7 @@ func TestCreateErrorFromDaemon(t *testing.T) { })) cmd.SetArgs([]string{"plugin-foo", tmpDir.Path()}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), "error creating plugin") } diff --git a/cli/command/plugin/disable_test.go b/cli/command/plugin/disable_test.go index cd78943c942d..40bfab72ea24 100644 --- a/cli/command/plugin/disable_test.go +++ b/cli/command/plugin/disable_test.go @@ -41,6 +41,7 @@ func TestPluginDisableErrors(t *testing.T) { })) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/plugin/enable_test.go b/cli/command/plugin/enable_test.go index 0f220547562a..20fa25f74098 100644 --- a/cli/command/plugin/enable_test.go +++ b/cli/command/plugin/enable_test.go @@ -51,6 +51,7 @@ func TestPluginEnableErrors(t *testing.T) { cmd.Flags().Set(key, value) } cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/plugin/formatter_test.go b/cli/command/plugin/formatter_test.go index 9d8b5c362c01..7fb81fce4226 100644 --- a/cli/command/plugin/formatter_test.go +++ b/cli/command/plugin/formatter_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package plugin diff --git a/cli/command/plugin/inspect.go b/cli/command/plugin/inspect.go index d910dc6e8f18..21ff99d9c771 100644 --- a/cli/command/plugin/inspect.go +++ b/cli/command/plugin/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package plugin diff --git a/cli/command/plugin/inspect_test.go b/cli/command/plugin/inspect_test.go index f5c2ad585760..2dcb8958891e 100644 --- a/cli/command/plugin/inspect_test.go +++ b/cli/command/plugin/inspect_test.go @@ -66,6 +66,7 @@ func TestInspectErrors(t *testing.T) { } for _, tc := range testCases { + tc := tc t.Run(tc.description, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{pluginInspectFunc: tc.inspectFunc}) cmd := newInspectCommand(cli) @@ -74,6 +75,7 @@ func TestInspectErrors(t *testing.T) { cmd.Flags().Set(key, value) } cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) }) } @@ -136,6 +138,7 @@ func TestInspect(t *testing.T) { } for _, tc := range testCases { + tc := tc t.Run(tc.description, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{pluginInspectFunc: tc.inspectFunc}) cmd := newInspectCommand(cli) diff --git a/cli/command/plugin/install_test.go b/cli/command/plugin/install_test.go index 03cad5f110e9..4eb55c1a9199 100644 --- a/cli/command/plugin/install_test.go +++ b/cli/command/plugin/install_test.go @@ -54,11 +54,15 @@ func TestInstallErrors(t *testing.T) { } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc}) - cmd := newInstallCommand(cli) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.description, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc}) + cmd := newInstallCommand(cli) + cmd.SetArgs(tc.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } @@ -90,16 +94,20 @@ func TestInstallContentTrustErrors(t *testing.T) { } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{ - pluginInstallFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) { - return nil, errors.New("should not try to install plugin") - }, - }, test.EnableContentTrust) - cli.SetNotaryClient(tc.notaryFunc) - cmd := newInstallCommand(cli) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.description, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + pluginInstallFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) { + return nil, errors.New("should not try to install plugin") + }, + }, test.EnableContentTrust) + cli.SetNotaryClient(tc.notaryFunc) + cmd := newInstallCommand(cli) + cmd.SetArgs(tc.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } @@ -130,10 +138,13 @@ func TestInstall(t *testing.T) { } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc}) - cmd := newInstallCommand(cli) - cmd.SetArgs(tc.args) - assert.NilError(t, cmd.Execute()) - assert.Check(t, strings.Contains(cli.OutBuffer().String(), tc.expectedOutput)) + tc := tc + t.Run(tc.description, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc}) + cmd := newInstallCommand(cli) + cmd.SetArgs(tc.args) + assert.NilError(t, cmd.Execute()) + assert.Check(t, strings.Contains(cli.OutBuffer().String(), tc.expectedOutput)) + }) } } diff --git a/cli/command/plugin/list_test.go b/cli/command/plugin/list_test.go index a5e4e71d31a0..07138856fab9 100644 --- a/cli/command/plugin/list_test.go +++ b/cli/command/plugin/list_test.go @@ -46,14 +46,18 @@ func TestListErrors(t *testing.T) { } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc}) - cmd := newListCommand(cli) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOut(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.description, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc}) + cmd := newListCommand(cli) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } @@ -162,13 +166,16 @@ func TestList(t *testing.T) { } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc}) - cmd := newListCommand(cli) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - assert.NilError(t, cmd.Execute()) - golden.Assert(t, cli.OutBuffer().String(), tc.golden) + tc := tc + t.Run(tc.description, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc}) + cmd := newListCommand(cli) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + assert.NilError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), tc.golden) + }) } } diff --git a/cli/command/plugin/remove.go b/cli/command/plugin/remove.go index 2614cab3ace4..36b0013105b4 100644 --- a/cli/command/plugin/remove.go +++ b/cli/command/plugin/remove.go @@ -2,6 +2,7 @@ package plugin import ( "context" + "errors" "fmt" "github.com/docker/cli/cli" @@ -36,17 +37,13 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command { } func runRemove(ctx context.Context, dockerCli command.Cli, opts *rmOptions) error { - var errs cli.Errors + var errs error for _, name := range opts.plugins { if err := dockerCli.Client().PluginRemove(ctx, name, types.PluginRemoveOptions{Force: opts.force}); err != nil { - errs = append(errs, err) + errs = errors.Join(errs, err) continue } - fmt.Fprintln(dockerCli.Out(), name) + _, _ = fmt.Fprintln(dockerCli.Out(), name) } - // Do not simplify to `return errs` because even if errs == nil, it is not a nil-error interface value. - if errs != nil { - return errs - } - return nil + return errs } diff --git a/cli/command/plugin/remove_test.go b/cli/command/plugin/remove_test.go index cf701cfa3b72..de40a28ade52 100644 --- a/cli/command/plugin/remove_test.go +++ b/cli/command/plugin/remove_test.go @@ -37,6 +37,7 @@ func TestRemoveErrors(t *testing.T) { cmd := newRemoveCommand(cli) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/plugin/upgrade_test.go b/cli/command/plugin/upgrade_test.go index f4795efa6635..006998b16ff5 100644 --- a/cli/command/plugin/upgrade_test.go +++ b/cli/command/plugin/upgrade_test.go @@ -32,6 +32,8 @@ func TestUpgradePromptTermination(t *testing.T) { // need to set a remote address that does not match the plugin // reference sent by the `pluginInspectFunc` cmd.SetArgs([]string{"foo/bar", "localhost:5000/foo/bar:v1.0.0"}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) test.TerminatePrompt(ctx, t, cmd, cli) golden.Assert(t, cli.OutBuffer().String(), "plugin-upgrade-terminate.golden") } diff --git a/cli/command/registry.go b/cli/command/registry.go index ba97861a6b49..cb966be1a77f 100644 --- a/cli/command/registry.go +++ b/cli/command/registry.go @@ -1,10 +1,8 @@ package command import ( - "bufio" "context" "fmt" - "io" "os" "runtime" "strings" @@ -18,24 +16,27 @@ import ( "github.com/docker/docker/api/types" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/registry" - "github.com/moby/term" "github.com/pkg/errors" ) -const 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/" +const ( + registerSuggest = "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." + 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 // for the given command. func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc { 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) isDefaultRegistry := indexServer == registry.IndexServer authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, indexServer, isDefaultRegistry) 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 { @@ -44,7 +45,7 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf default: } - err = ConfigureAuth(cli, "", "", &authConfig, isDefaultRegistry) + authConfig, err = PromptUserForCredentials(ctx, cli, "", "", authConfig.Username, indexServer) if err != nil { return "", err } @@ -89,8 +90,33 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve return registrytypes.AuthConfig(authconfig), nil } -// ConfigureAuth handles prompting of user's username and password if needed -func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *registrytypes.AuthConfig, isDefaultRegistry bool) error { +// ConfigureAuth handles prompting of user's username and password if needed. +// +// Deprecated: use [PromptUserForCredentials] instead. +func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authConfig *registrytypes.AuthConfig, _ bool) error { + defaultUsername := authConfig.Username + serverAddress := authConfig.ServerAddress + + newAuthConfig, err := PromptUserForCredentials(ctx, cli, flUser, flPassword, defaultUsername, serverAddress) + if err != nil { + return err + } + + authConfig.Username = newAuthConfig.Username + authConfig.Password = newAuthConfig.Password + return nil +} + +// PromptUserForCredentials handles the CLI prompt for the user to input +// credentials. +// If argUser is not empty, then the user is only prompted for their password. +// If argPassword is not empty, then the user is only prompted for their username +// If neither argUser nor argPassword are empty, then the user is not prompted and +// an AuthConfig is returned with those values. +// 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 +// username. +func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (registrytypes.AuthConfig, error) { // On Windows, force the use of the regular OS stdin stream. // // See: @@ -103,84 +129,71 @@ func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *registrytypes cli.SetIn(streams.NewIn(os.Stdin)) } - // Some links documenting this: - // - https://code.google.com/archive/p/mintty/issues/56 - // - https://github.com/docker/docker/issues/15272 - // - https://mintty.github.io/ (compatibility) - // Linux will hit this if you attempt `cat | docker login`, and Windows - // will hit this if you attempt docker login from mintty where stdin - // is a pipe, not a character based console. - if flPassword == "" && !cli.In().IsTerminal() { - return errors.Errorf("Error: Cannot perform an interactive login from a non TTY device") - } - - authconfig.Username = strings.TrimSpace(authconfig.Username) - - if flUser = strings.TrimSpace(flUser); flUser == "" { - if isDefaultRegistry { - // if this is a default registry (docker hub), then display the following message. - fmt.Fprintln(cli.Out(), "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.") + argUser = strings.TrimSpace(argUser) + if argUser == "" { + if serverAddress == registry.IndexServer { + // When signing in to the default (Docker Hub) registry, we display + // hints for creating an account, and (if hints are enabled), using + // a token instead of a password. + _, _ = fmt.Fprintln(cli.Out(), registerSuggest) if hints.Enabled() { - fmt.Fprintln(cli.Out(), patSuggest) - fmt.Fprintln(cli.Out()) + _, _ = fmt.Fprintln(cli.Out(), patSuggest) + _, _ = fmt.Fprintln(cli.Out()) } } - promptWithDefault(cli.Out(), "Username", authconfig.Username) + + var prompt string + defaultUsername = strings.TrimSpace(defaultUsername) + if defaultUsername == "" { + prompt = "Username: " + } else { + prompt = fmt.Sprintf("Username (%s): ", defaultUsername) + } + var err error - flUser, err = readInput(cli.In()) + argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt) if err != nil { - return err + return registrytypes.AuthConfig{}, err } - if flUser == "" { - flUser = authconfig.Username + if argUser == "" { + argUser = defaultUsername + } + if argUser == "" { + return registrytypes.AuthConfig{}, errors.Errorf("Error: Non-null Username Required") } } - if flUser == "" { - return errors.Errorf("Error: Non-null Username Required") - } - if flPassword == "" { - oldState, err := term.SaveState(cli.In().FD()) + + argPassword = strings.TrimSpace(argPassword) + if argPassword == "" { + restoreInput, err := DisableInputEcho(cli.In()) if err != nil { - return err + return registrytypes.AuthConfig{}, err } - fmt.Fprintf(cli.Out(), "Password: ") - _ = term.DisableEcho(cli.In().FD(), oldState) defer func() { - _ = term.RestoreTerminal(cli.In().FD(), oldState) + 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) + } }() - flPassword, err = readInput(cli.In()) + + argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ") if err != nil { - return err + return registrytypes.AuthConfig{}, err } - fmt.Fprint(cli.Out(), "\n") - if flPassword == "" { - return errors.Errorf("Error: Password Required") + _, _ = fmt.Fprintln(cli.Out()) + if argPassword == "" { + return registrytypes.AuthConfig{}, errors.Errorf("Error: Password Required") } } - authconfig.Username = flUser - authconfig.Password = flPassword - - return nil -} - -// readInput reads, and returns user input from in. It tries to return a -// single line, not including the end-of-line bytes, and trims leading -// and trailing whitespace. -func readInput(in io.Reader) (string, error) { - line, _, err := bufio.NewReader(in).ReadLine() - if err != nil { - return "", errors.Wrap(err, "error while reading input") - } - return strings.TrimSpace(string(line)), nil -} - -func promptWithDefault(out io.Writer, prompt string, configDefault string) { - if configDefault == "" { - fmt.Fprintf(out, "%s: ", prompt) - } else { - fmt.Fprintf(out, "%s (%s): ", prompt, configDefault) - } + return registrytypes.AuthConfig{ + Username: argUser, + Password: argPassword, + ServerAddress: serverAddress, + }, nil } // RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete diff --git a/cli/command/registry/login.go b/cli/command/registry/login.go index 69476b6ea0a2..58bf2477dfe6 100644 --- a/cli/command/registry/login.go +++ b/cli/command/registry/login.go @@ -4,12 +4,15 @@ import ( "context" "fmt" "io" + "os" + "strconv" "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" configtypes "github.com/docker/cli/cli/config/types" + "github.com/docker/cli/cli/internal/oauth/manager" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/client" "github.com/docker/docker/errdefs" @@ -36,8 +39,8 @@ func NewLoginCommand(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "login [OPTIONS] [SERVER]", - Short: "Log in to a registry", - Long: "Log in to a registry.\nIf no server is specified, the default is defined by the daemon.", + Short: "Authenticate to a registry", + Long: "Authenticate to a registry.\nDefaults to Docker Hub if no server is specified.", Args: cli.RequiresMaxArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 0 { @@ -100,80 +103,176 @@ func verifyloginOptions(dockerCli command.Cli, opts *loginOptions) error { return nil } -func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error { //nolint:gocyclo - clnt := dockerCli.Client() +func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error { if err := verifyloginOptions(dockerCli, &opts); err != nil { return err } var ( serverAddress string - response registrytypes.AuthenticateOKBody + response *registrytypes.AuthenticateOKBody ) if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace { serverAddress = opts.serverAddress } else { serverAddress = registry.IndexServer } - isDefaultRegistry := serverAddress == registry.IndexServer + + // attempt login with current (stored) credentials authConfig, err := command.GetDefaultAuthConfig(dockerCli.ConfigFile(), opts.user == "" && opts.password == "", serverAddress, isDefaultRegistry) if err == nil && authConfig.Username != "" && authConfig.Password != "" { - response, err = loginWithCredStoreCreds(ctx, dockerCli, &authConfig) + response, err = loginWithStoredCredentials(ctx, dockerCli, authConfig) } + + // if we failed to authenticate with stored credentials (or didn't have stored credentials), + // prompt the user for new credentials if err != nil || authConfig.Username == "" || authConfig.Password == "" { - err = command.ConfigureAuth(dockerCli, opts.user, opts.password, &authConfig, isDefaultRegistry) + response, err = loginUser(ctx, dockerCli, opts, authConfig.Username, authConfig.ServerAddress) if err != nil { return err } + } + + if response != nil && response.Status != "" { + _, _ = fmt.Fprintln(dockerCli.Out(), response.Status) + } + return nil +} - response, err = clnt.RegistryLogin(ctx, authConfig) - if err != nil && client.IsErrConnectionFailed(err) { - // If the server isn't responding (yet) attempt to login purely client side - response, err = loginClientSide(ctx, authConfig) +func loginWithStoredCredentials(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (*registrytypes.AuthenticateOKBody, error) { + _, _ = fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n") + response, err := dockerCli.Client().RegistryLogin(ctx, authConfig) + if err != nil { + if errdefs.IsUnauthorized(err) { + _, _ = fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n") + } else { + _, _ = fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err) } - // If we (still) have an error, give up + } + + if response.IdentityToken != "" { + authConfig.Password = "" + authConfig.IdentityToken = response.IdentityToken + } + + if err := storeCredentials(dockerCli, authConfig); err != nil { + return nil, err + } + + return &response, err +} + +const OauthLoginEscapeHatchEnvVar = "DOCKER_CLI_DISABLE_OAUTH_LOGIN" + +func isOauthLoginDisabled() bool { + if v := os.Getenv(OauthLoginEscapeHatchEnvVar); v != "" { + enabled, err := strconv.ParseBool(v) if err != nil { - return err + return false + } + return enabled + } + return false +} + +func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) { + // Some links documenting this: + // - https://code.google.com/archive/p/mintty/issues/56 + // - https://github.com/docker/docker/issues/15272 + // - https://mintty.github.io/ (compatibility) + // Linux will hit this if you attempt `cat | docker login`, and Windows + // will hit this if you attempt docker login from mintty where stdin + // is a pipe, not a character based console. + if (opts.user == "" || opts.password == "") && !dockerCli.In().IsTerminal() { + return nil, errors.Errorf("Error: Cannot perform an interactive login from a non TTY device") + } + + // If we're logging into the index server and the user didn't provide a username or password, use the device flow + if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" && !isOauthLoginDisabled() { + response, err := loginWithDeviceCodeFlow(ctx, dockerCli) + // if the error represents a failure to initiate the device-code flow, + // then we fallback to regular cli credentials login + if !errors.Is(err, manager.ErrDeviceLoginStartFail) { + return response, err } + 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) +} + +func loginWithUsernameAndPassword(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) { + // Prompt user for credentials + authConfig, err := command.PromptUserForCredentials(ctx, dockerCli, opts.user, opts.password, defaultUsername, serverAddress) + if err != nil { + return nil, err + } + + response, err := loginWithRegistry(ctx, dockerCli, authConfig) + if err != nil { + return nil, err + } + if response.IdentityToken != "" { authConfig.Password = "" authConfig.IdentityToken = response.IdentityToken } + if err = storeCredentials(dockerCli, authConfig); err != nil { + return nil, err + } + + return &response, nil +} + +func loginWithDeviceCodeFlow(ctx context.Context, dockerCli command.Cli) (*registrytypes.AuthenticateOKBody, error) { + store := dockerCli.ConfigFile().GetCredentialsStore(registry.IndexServer) + authConfig, err := manager.NewManager(store).LoginDevice(ctx, dockerCli.Err()) + if err != nil { + return nil, err + } - creds := dockerCli.ConfigFile().GetCredentialsStore(serverAddress) + response, err := loginWithRegistry(ctx, dockerCli, registrytypes.AuthConfig(*authConfig)) + if err != nil { + return nil, err + } + + if err = storeCredentials(dockerCli, registrytypes.AuthConfig(*authConfig)); err != nil { + return nil, err + } + + return &response, nil +} + +func storeCredentials(dockerCli command.Cli, authConfig registrytypes.AuthConfig) error { + creds := dockerCli.ConfigFile().GetCredentialsStore(authConfig.ServerAddress) store, isDefault := creds.(isFileStore) // Display a warning if we're storing the users password (not a token) if isDefault && authConfig.Password != "" { - err = displayUnencryptedWarning(dockerCli, store.GetFilename()) + err := displayUnencryptedWarning(dockerCli, store.GetFilename()) if err != nil { return err } } - if err := creds.Store(configtypes.AuthConfig(authConfig)); err != nil { return errors.Errorf("Error saving credentials: %v", err) } - if response.Status != "" { - fmt.Fprintln(dockerCli.Out(), response.Status) - } return nil } -func loginWithCredStoreCreds(ctx context.Context, dockerCli command.Cli, authConfig *registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) { - fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n") - cliClient := dockerCli.Client() - response, err := cliClient.RegistryLogin(ctx, *authConfig) +func loginWithRegistry(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) { + response, err := dockerCli.Client().RegistryLogin(ctx, authConfig) + if err != nil && client.IsErrConnectionFailed(err) { + // If the server isn't responding (yet) attempt to login purely client side + response, err = loginClientSide(ctx, authConfig) + } + // If we (still) have an error, give up if err != nil { - if errdefs.IsUnauthorized(err) { - fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n") - } else { - fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err) - } + return registrytypes.AuthenticateOKBody{}, err } - return response, err + + return response, nil } func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) { diff --git a/cli/command/registry/login_test.go b/cli/command/registry/login_test.go index 9d87ad766773..0dbfaa65706d 100644 --- a/cli/command/registry/login_test.go +++ b/cli/command/registry/login_test.go @@ -6,13 +6,17 @@ import ( "errors" "fmt" "testing" + "time" + "github.com/creack/pty" + "github.com/docker/cli/cli/command" configtypes "github.com/docker/cli/cli/config/types" "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/test" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/system" "github.com/docker/docker/client" + "github.com/docker/docker/registry" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/fs" @@ -71,7 +75,7 @@ func TestLoginWithCredStoreCreds(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) errBuf := new(bytes.Buffer) cli.SetErr(streams.NewOut(errBuf)) - loginWithCredStoreCreds(ctx, cli, &tc.inputAuthConfig) + loginWithStoredCredentials(ctx, cli, tc.inputAuthConfig) outputString := cli.OutBuffer().String() assert.Check(t, is.Equal(tc.expectedMsg, outputString)) errorString := errBuf.String() @@ -80,88 +84,207 @@ func TestLoginWithCredStoreCreds(t *testing.T) { } func TestRunLogin(t *testing.T) { - const ( - storedServerAddress = "reg1" - validUsername = "u1" - validPassword = "p1" - validPassword2 = "p2" - ) - - validAuthConfig := configtypes.AuthConfig{ - ServerAddress: storedServerAddress, - Username: validUsername, - Password: validPassword, - } - expiredAuthConfig := configtypes.AuthConfig{ - ServerAddress: storedServerAddress, - Username: validUsername, - Password: expiredPassword, - } - validIdentityToken := configtypes.AuthConfig{ - ServerAddress: storedServerAddress, - Username: validUsername, - IdentityToken: useToken, - } testCases := []struct { - doc string - inputLoginOption loginOptions - inputStoredCred *configtypes.AuthConfig - expectedErr string - expectedSavedCred configtypes.AuthConfig + doc string + priorCredentials map[string]configtypes.AuthConfig + input loginOptions + expectedCredentials map[string]configtypes.AuthConfig + expectedErr string }{ { doc: "valid auth from store", - inputLoginOption: loginOptions{ - serverAddress: storedServerAddress, + priorCredentials: map[string]configtypes.AuthConfig{ + "reg1": { + Username: "my-username", + Password: "a-password", + ServerAddress: "reg1", + }, + }, + input: loginOptions{ + serverAddress: "reg1", + }, + expectedCredentials: map[string]configtypes.AuthConfig{ + "reg1": { + Username: "my-username", + Password: "a-password", + ServerAddress: "reg1", + }, + }, + }, + { + doc: "expired auth from store", + priorCredentials: map[string]configtypes.AuthConfig{ + "reg1": { + Username: "my-username", + Password: expiredPassword, + ServerAddress: "reg1", + }, + }, + input: loginOptions{ + serverAddress: "reg1", }, - inputStoredCred: &validAuthConfig, - expectedSavedCred: validAuthConfig, + expectedErr: "Error: Cannot perform an interactive login from a non TTY device", }, { - doc: "expired auth", - inputLoginOption: loginOptions{ - serverAddress: storedServerAddress, + doc: "store valid username and password", + priorCredentials: map[string]configtypes.AuthConfig{}, + input: loginOptions{ + serverAddress: "reg1", + user: "my-username", + password: "p2", + }, + expectedCredentials: map[string]configtypes.AuthConfig{ + "reg1": { + Username: "my-username", + Password: "p2", + ServerAddress: "reg1", + }, }, - inputStoredCred: &expiredAuthConfig, - expectedErr: "Error: Cannot perform an interactive login from a non TTY device", }, { - doc: "valid username and password", - inputLoginOption: loginOptions{ - serverAddress: storedServerAddress, - user: validUsername, - password: validPassword2, + doc: "unknown user w/ prior credentials", + priorCredentials: map[string]configtypes.AuthConfig{ + "reg1": { + Username: "my-username", + Password: "a-password", + ServerAddress: "reg1", + }, + }, + input: loginOptions{ + serverAddress: "reg1", + user: unknownUser, + password: "a-password", }, - inputStoredCred: &validAuthConfig, - expectedSavedCred: configtypes.AuthConfig{ - ServerAddress: storedServerAddress, - Username: validUsername, - Password: validPassword2, + expectedErr: errUnknownUser, + expectedCredentials: map[string]configtypes.AuthConfig{ + "reg1": { + Username: "a-password", + Password: "a-password", + ServerAddress: "reg1", + }, }, }, { - doc: "unknown user", - inputLoginOption: loginOptions{ - serverAddress: storedServerAddress, + doc: "unknown user w/o prior credentials", + priorCredentials: map[string]configtypes.AuthConfig{}, + input: loginOptions{ + serverAddress: "reg1", user: unknownUser, - password: validPassword, + password: "a-password", }, - inputStoredCred: &validAuthConfig, - expectedErr: errUnknownUser, + expectedErr: errUnknownUser, + expectedCredentials: map[string]configtypes.AuthConfig{}, }, { - doc: "valid token", - inputLoginOption: loginOptions{ - serverAddress: storedServerAddress, - user: validUsername, + doc: "store valid token", + priorCredentials: map[string]configtypes.AuthConfig{}, + input: loginOptions{ + serverAddress: "reg1", + user: "my-username", password: useToken, }, - inputStoredCred: &validIdentityToken, - expectedSavedCred: validIdentityToken, + expectedCredentials: map[string]configtypes.AuthConfig{ + "reg1": { + Username: "my-username", + IdentityToken: useToken, + ServerAddress: "reg1", + }, + }, + }, + { + doc: "valid token from store", + priorCredentials: map[string]configtypes.AuthConfig{ + "reg1": { + Username: "my-username", + Password: useToken, + ServerAddress: "reg1", + }, + }, + input: loginOptions{ + serverAddress: "reg1", + }, + expectedCredentials: map[string]configtypes.AuthConfig{ + "reg1": { + Username: "my-username", + IdentityToken: useToken, + ServerAddress: "reg1", + }, + }, + }, + { + doc: "no registry specified defaults to index server", + priorCredentials: map[string]configtypes.AuthConfig{}, + input: loginOptions{ + user: "my-username", + password: "my-password", + }, + expectedCredentials: map[string]configtypes.AuthConfig{ + registry.IndexServer: { + Username: "my-username", + Password: "my-password", + ServerAddress: registry.IndexServer, + }, + }, + }, + { + doc: "registry-1.docker.io", + priorCredentials: map[string]configtypes.AuthConfig{}, + input: loginOptions{ + serverAddress: "registry-1.docker.io", + user: "my-username", + password: "my-password", + }, + expectedCredentials: map[string]configtypes.AuthConfig{ + "registry-1.docker.io": { + Username: "my-username", + Password: "my-password", + ServerAddress: "registry-1.docker.io", + }, + }, + }, + // Regression test for https://github.com/docker/cli/issues/5382 + { + doc: "sanitizes server address to remove repo", + priorCredentials: map[string]configtypes.AuthConfig{}, + input: loginOptions{ + serverAddress: "registry-1.docker.io/bork/test", + user: "my-username", + password: "a-password", + }, + expectedCredentials: map[string]configtypes.AuthConfig{ + "registry-1.docker.io": { + Username: "my-username", + Password: "a-password", + ServerAddress: "registry-1.docker.io", + }, + }, + }, + // Regression test for https://github.com/docker/cli/issues/5382 + { + doc: "updates credential if server address includes repo", + priorCredentials: map[string]configtypes.AuthConfig{ + "registry-1.docker.io": { + Username: "my-username", + Password: "a-password", + ServerAddress: "registry-1.docker.io", + }, + }, + input: loginOptions{ + serverAddress: "registry-1.docker.io/bork/test", + user: "my-username", + password: "new-password", + }, + expectedCredentials: map[string]configtypes.AuthConfig{ + "registry-1.docker.io": { + Username: "my-username", + Password: "new-password", + ServerAddress: "registry-1.docker.io", + }, + }, }, } + for _, tc := range testCases { - tc := tc t.Run(tc.doc, func(t *testing.T) { tmpFile := fs.NewFile(t, "test-run-login") defer tmpFile.Remove() @@ -169,19 +292,246 @@ func TestRunLogin(t *testing.T) { configfile := cli.ConfigFile() configfile.Filename = tmpFile.Path() - if tc.inputStoredCred != nil { - cred := *tc.inputStoredCred - assert.NilError(t, configfile.GetCredentialsStore(cred.ServerAddress).Store(cred)) + for _, priorCred := range tc.priorCredentials { + assert.NilError(t, configfile.GetCredentialsStore(priorCred.ServerAddress).Store(priorCred)) } - loginErr := runLogin(context.Background(), cli, tc.inputLoginOption) + storedCreds, err := configfile.GetAllCredentials() + assert.NilError(t, err) + assert.DeepEqual(t, storedCreds, tc.priorCredentials) + + loginErr := runLogin(context.Background(), cli, tc.input) if tc.expectedErr != "" { assert.Error(t, loginErr, tc.expectedErr) return } assert.NilError(t, loginErr) - savedCred, credStoreErr := configfile.GetCredentialsStore(tc.inputStoredCred.ServerAddress).Get(tc.inputStoredCred.ServerAddress) - assert.Check(t, credStoreErr) - assert.DeepEqual(t, tc.expectedSavedCred, savedCred) + + outputCreds, err := configfile.GetAllCredentials() + assert.Check(t, err) + assert.DeepEqual(t, outputCreds, tc.expectedCredentials) }) } } + +func TestLoginNonInteractive(t *testing.T) { + t.Run("no prior credentials", func(t *testing.T) { + testCases := []struct { + doc string + username bool + password bool + expectedErr string + }{ + { + doc: "success - w/ user w/ password", + username: true, + password: true, + }, + { + doc: "error - w/o user w/o pass ", + username: false, + password: false, + expectedErr: "Error: Cannot perform an interactive login from a non TTY device", + }, + { + doc: "error - w/ user w/o pass", + username: true, + password: false, + expectedErr: "Error: Cannot perform an interactive login from a non TTY device", + }, + { + doc: "error - w/o user w/ pass", + username: false, + password: true, + expectedErr: "Error: Cannot perform an interactive login from a non TTY device", + }, + } + + // "" meaning default registry + registries := []string{"", "my-registry.com"} + + for _, registry := range registries { + for _, tc := range testCases { + t.Run(tc.doc, func(t *testing.T) { + tmpFile := fs.NewFile(t, "test-run-login") + defer tmpFile.Remove() + cli := test.NewFakeCli(&fakeClient{}) + configfile := cli.ConfigFile() + configfile.Filename = tmpFile.Path() + options := loginOptions{ + serverAddress: registry, + } + if tc.username { + options.user = "my-username" + } + if tc.password { + options.password = "my-password" + } + + loginErr := runLogin(context.Background(), cli, options) + if tc.expectedErr != "" { + assert.Error(t, loginErr, tc.expectedErr) + return + } + assert.NilError(t, loginErr) + }) + } + } + }) + + t.Run("w/ prior credentials", func(t *testing.T) { + testCases := []struct { + doc string + username bool + password bool + expectedErr string + }{ + { + doc: "success - w/ user w/ password", + username: true, + password: true, + }, + { + doc: "success - w/o user w/o pass ", + username: false, + password: false, + }, + { + doc: "error - w/ user w/o pass", + username: true, + password: false, + expectedErr: "Error: Cannot perform an interactive login from a non TTY device", + }, + { + doc: "error - w/o user w/ pass", + username: false, + password: true, + expectedErr: "Error: Cannot perform an interactive login from a non TTY device", + }, + } + + // "" meaning default registry + registries := []string{"", "my-registry.com"} + + for _, registry := range registries { + for _, tc := range testCases { + t.Run(tc.doc, func(t *testing.T) { + tmpFile := fs.NewFile(t, "test-run-login") + defer tmpFile.Remove() + cli := test.NewFakeCli(&fakeClient{}) + configfile := cli.ConfigFile() + configfile.Filename = tmpFile.Path() + serverAddress := registry + if serverAddress == "" { + serverAddress = "https://index.docker.io/v1/" + } + assert.NilError(t, configfile.GetCredentialsStore(serverAddress).Store(configtypes.AuthConfig{ + Username: "my-username", + Password: "my-password", + ServerAddress: serverAddress, + })) + + options := loginOptions{ + serverAddress: registry, + } + if tc.username { + options.user = "my-username" + } + if tc.password { + options.password = "my-password" + } + + loginErr := runLogin(context.Background(), cli, options) + if tc.expectedErr != "" { + assert.Error(t, loginErr, tc.expectedErr) + return + } + assert.NilError(t, loginErr) + }) + } + } + }) +} + +func TestLoginTermination(t *testing.T) { + p, tty, err := pty.Open() + assert.NilError(t, err) + + t.Cleanup(func() { + _ = tty.Close() + _ = p.Close() + }) + + cli := test.NewFakeCli(&fakeClient{}, func(fc *test.FakeCli) { + fc.SetOut(streams.NewOut(tty)) + fc.SetIn(streams.NewIn(tty)) + }) + tmpFile := fs.NewFile(t, "test-login-termination") + defer tmpFile.Remove() + + configFile := cli.ConfigFile() + configFile.Filename = tmpFile.Path() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + runErr := make(chan error) + go func() { + runErr <- runLogin(ctx, cli, loginOptions{ + user: "test-user", + }) + }() + + // Let the prompt get canceled by the context + cancel() + + select { + case <-time.After(1 * time.Second): + t.Fatal("timed out after 1 second. `runLogin` did not return") + case err := <-runErr: + assert.ErrorIs(t, err, command.ErrPromptTerminated) + } +} + +func TestIsOauthLoginDisabled(t *testing.T) { + testCases := []struct { + envVar string + disabled bool + }{ + { + envVar: "", + disabled: false, + }, + { + envVar: "bork", + disabled: false, + }, + { + envVar: "0", + disabled: false, + }, + { + envVar: "false", + disabled: false, + }, + { + envVar: "true", + disabled: true, + }, + { + envVar: "TRUE", + disabled: true, + }, + { + envVar: "1", + disabled: true, + }, + } + + for _, tc := range testCases { + t.Setenv(OauthLoginEscapeHatchEnvVar, tc.envVar) + + disabled := isOauthLoginDisabled() + + assert.Equal(t, disabled, tc.disabled) + } +} diff --git a/cli/command/registry/logout.go b/cli/command/registry/logout.go index 580a3b8746dc..4eb479f3e55b 100644 --- a/cli/command/registry/logout.go +++ b/cli/command/registry/logout.go @@ -7,6 +7,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config/credentials" + "github.com/docker/cli/cli/internal/oauth/manager" "github.com/docker/docker/registry" "github.com/spf13/cobra" ) @@ -34,7 +35,7 @@ func NewLogoutCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runLogout(_ context.Context, dockerCli command.Cli, serverAddress string) error { +func runLogout(ctx context.Context, dockerCli command.Cli, serverAddress string) error { var isDefaultRegistry bool if serverAddress == "" { @@ -53,6 +54,13 @@ func runLogout(_ context.Context, dockerCli command.Cli, serverAddress string) e regsToLogout = append(regsToLogout, hostnameAddress, "http://"+hostnameAddress, "https://"+hostnameAddress) } + if isDefaultRegistry { + store := dockerCli.ConfigFile().GetCredentialsStore(registry.IndexServer) + if err := manager.NewManager(store).Logout(ctx); err != nil { + fmt.Fprintf(dockerCli.Err(), "WARNING: %v\n", err) + } + } + fmt.Fprintf(dockerCli.Out(), "Removing login credentials for %s\n", hostnameAddress) errs := make(map[string]error) for _, r := range regsToLogout { diff --git a/cli/command/secret/create_test.go b/cli/command/secret/create_test.go index 6af3aa97f4ab..41c7b853da08 100644 --- a/cli/command/secret/create_test.go +++ b/cli/command/secret/create_test.go @@ -49,6 +49,7 @@ func TestSecretCreateErrors(t *testing.T) { ) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/secret/inspect.go b/cli/command/secret/inspect.go index df448155ec8b..5559d43d383e 100644 --- a/cli/command/secret/inspect.go +++ b/cli/command/secret/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package secret diff --git a/cli/command/secret/inspect_test.go b/cli/command/secret/inspect_test.go index 47a28cb8d010..ca509c12494e 100644 --- a/cli/command/secret/inspect_test.go +++ b/cli/command/secret/inspect_test.go @@ -61,6 +61,7 @@ func TestSecretInspectErrors(t *testing.T) { assert.Check(t, cmd.Flags().Set(key, value)) } cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } @@ -92,13 +93,16 @@ func TestSecretInspectWithoutFormat(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{ - secretInspectFunc: tc.secretInspectFunc, + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + secretInspectFunc: tc.secretInspectFunc, + }) + cmd := newSecretInspectCommand(cli) + cmd.SetArgs(tc.args) + assert.NilError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("secret-inspect-without-format.%s.golden", tc.name)) }) - cmd := newSecretInspectCommand(cli) - cmd.SetArgs(tc.args) - assert.NilError(t, cmd.Execute()) - golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("secret-inspect-without-format.%s.golden", tc.name)) } } @@ -128,14 +132,17 @@ func TestSecretInspectWithFormat(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{ - secretInspectFunc: tc.secretInspectFunc, + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + secretInspectFunc: tc.secretInspectFunc, + }) + cmd := newSecretInspectCommand(cli) + cmd.SetArgs(tc.args) + assert.Check(t, cmd.Flags().Set("format", tc.format)) + assert.NilError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("secret-inspect-with-format.%s.golden", tc.name)) }) - cmd := newSecretInspectCommand(cli) - cmd.SetArgs(tc.args) - assert.Check(t, cmd.Flags().Set("format", tc.format)) - assert.NilError(t, cmd.Execute()) - golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("secret-inspect-with-format.%s.golden", tc.name)) } } diff --git a/cli/command/secret/ls_test.go b/cli/command/secret/ls_test.go index 0579612b96a9..161e32f2ce64 100644 --- a/cli/command/secret/ls_test.go +++ b/cli/command/secret/ls_test.go @@ -42,6 +42,7 @@ func TestSecretListErrors(t *testing.T) { ) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } diff --git a/cli/command/secret/remove_test.go b/cli/command/secret/remove_test.go index 8600a98fd314..6b0dfcc2cdff 100644 --- a/cli/command/secret/remove_test.go +++ b/cli/command/secret/remove_test.go @@ -38,6 +38,7 @@ func TestSecretRemoveErrors(t *testing.T) { ) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } @@ -74,6 +75,7 @@ func TestSecretRemoveContinueAfterError(t *testing.T) { cmd := newSecretRemoveCommand(cli) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) cmd.SetArgs(names) assert.Error(t, cmd.Execute(), "error removing secret: foo") assert.Check(t, is.DeepEqual(names, removedSecrets)) diff --git a/cli/command/service/create.go b/cli/command/service/create.go index 6e49558609a1..b1bed6619594 100644 --- a/cli/command/service/create.go +++ b/cli/command/service/create.go @@ -68,6 +68,8 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command { flags.SetAnnotation(flagSysCtl, "version", []string{"1.40"}) flags.Var(&opts.ulimits, flagUlimit, "Ulimit options") flags.SetAnnotation(flagUlimit, "version", []string{"1.41"}) + flags.Int64Var(&opts.oomScoreAdj, flagOomScoreAdj, 0, "Tune host's OOM preferences (-1000 to 1000) ") + flags.SetAnnotation(flagOomScoreAdj, "version", []string{"1.46"}) flags.Var(cliopts.NewListOptsRef(&opts.resources.resGenericResources, ValidateSingleGenericResource), "generic-resource", "User defined resources") flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"}) diff --git a/cli/command/service/formatter_test.go b/cli/command/service/formatter_test.go index adb09476ad29..fd07f0ad72ec 100644 --- a/cli/command/service/formatter_test.go +++ b/cli/command/service/formatter_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package service diff --git a/cli/command/service/inspect.go b/cli/command/service/inspect.go index fcd85c120927..28b957f77310 100644 --- a/cli/command/service/inspect.go +++ b/cli/command/service/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package service diff --git a/cli/command/service/inspect_test.go b/cli/command/service/inspect_test.go index cd088c77a221..b1da0dafaf54 100644 --- a/cli/command/service/inspect_test.go +++ b/cli/command/service/inspect_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package service diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index c0a30674ec9e..9298cfb13145 100644 --- a/cli/command/service/opts.go +++ b/cli/command/service/opts.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package service @@ -529,6 +529,7 @@ type serviceOptions struct { capAdd opts.ListOpts capDrop opts.ListOpts ulimits opts.UlimitOpt + oomScoreAdj int64 resources resourceOptions stopGrace opts.DurationOpt @@ -747,6 +748,7 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N CapabilityAdd: capAdd, CapabilityDrop: capDrop, Ulimits: options.ulimits.GetList(), + OomScoreAdj: options.oomScoreAdj, }, Networks: networks, Resources: resources, @@ -1043,6 +1045,7 @@ const ( flagUlimit = "ulimit" flagUlimitAdd = "ulimit-add" flagUlimitRemove = "ulimit-rm" + flagOomScoreAdj = "oom-score-adj" ) func validateAPIVersion(c swarm.ServiceSpec, serverAPIVersion string) error { diff --git a/cli/command/service/remove.go b/cli/command/service/remove.go index c6e938e13b54..efbd5f9ad063 100644 --- a/cli/command/service/remove.go +++ b/cli/command/service/remove.go @@ -39,10 +39,10 @@ func runRemove(ctx context.Context, dockerCli command.Cli, sids []string) error errs = append(errs, err.Error()) continue } - fmt.Fprintf(dockerCli.Out(), "%s\n", sid) + _, _ = fmt.Fprintf(dockerCli.Out(), "%s\n", sid) } if len(errs) > 0 { - return errors.Errorf(strings.Join(errs, "\n")) + return errors.New(strings.Join(errs, "\n")) } return nil } diff --git a/cli/command/service/rollback_test.go b/cli/command/service/rollback_test.go index 9c5b97bc7674..742d9f99c404 100644 --- a/cli/command/service/rollback_test.go +++ b/cli/command/service/rollback_test.go @@ -91,14 +91,18 @@ func TestRollbackWithErrors(t *testing.T) { } for _, tc := range testCases { - cmd := newRollbackCommand( - test.NewFakeCli(&fakeClient{ - serviceInspectWithRawFunc: tc.serviceInspectWithRawFunc, - serviceUpdateFunc: tc.serviceUpdateFunc, - })) - cmd.SetArgs(tc.args) - cmd.Flags().Set("quiet", "true") - cmd.SetOut(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cmd := newRollbackCommand( + test.NewFakeCli(&fakeClient{ + serviceInspectWithRawFunc: tc.serviceInspectWithRawFunc, + serviceUpdateFunc: tc.serviceUpdateFunc, + })) + cmd.SetArgs(tc.args) + cmd.Flags().Set("quiet", "true") + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } diff --git a/cli/command/service/scale.go b/cli/command/service/scale.go index fd863049893d..688a3ff2d098 100644 --- a/cli/command/service/scale.go +++ b/cli/command/service/scale.go @@ -90,7 +90,7 @@ func runScale(ctx context.Context, dockerCli command.Cli, options *scaleOptions, if len(errs) == 0 { return nil } - return errors.Errorf(strings.Join(errs, "\n")) + return errors.New(strings.Join(errs, "\n")) } func runServiceScale(ctx context.Context, dockerCli command.Cli, serviceID string, scale uint64) error { diff --git a/cli/command/service/update.go b/cli/command/service/update.go index e02e19ccc842..d8f89185aeec 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -108,6 +108,8 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { flags.SetAnnotation(flagUlimitAdd, "version", []string{"1.41"}) flags.Var(newListOptsVar(), flagUlimitRemove, "Remove a ulimit option") flags.SetAnnotation(flagUlimitRemove, "version", []string{"1.41"}) + flags.Int64Var(&options.oomScoreAdj, flagOomScoreAdj, 0, "Tune host's OOM preferences (-1000 to 1000) ") + flags.SetAnnotation(flagOomScoreAdj, "version", []string{"1.46"}) // Add needs parsing, Remove only needs the key flags.Var(newListOptsVar(), flagGenericResourcesRemove, "Remove a Generic resource") @@ -367,6 +369,10 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags updateInt64Value(flagReserveMemory, &task.Resources.Reservations.MemoryBytes) } + if anyChanged(flags, flagOomScoreAdj) { + updateInt64(flagOomScoreAdj, &task.ContainerSpec.OomScoreAdj) + } + if err := addGenericResources(flags, task); err != nil { return err } diff --git a/cli/command/stack/config_test.go b/cli/command/stack/config_test.go index 4a3f88e46afc..ed3dcc9a44c6 100644 --- a/cli/command/stack/config_test.go +++ b/cli/command/stack/config_test.go @@ -13,6 +13,7 @@ import ( func TestConfigWithEmptyComposeFile(t *testing.T) { cmd := newConfigCommand(test.NewFakeCli(&fakeClient{})) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), `Specify a Compose file`) } diff --git a/cli/command/stack/deploy_test.go b/cli/command/stack/deploy_test.go index 86215bdc32e1..df731256bde5 100644 --- a/cli/command/stack/deploy_test.go +++ b/cli/command/stack/deploy_test.go @@ -12,6 +12,7 @@ func TestDeployWithEmptyName(t *testing.T) { cmd := newDeployCommand(test.NewFakeCli(&fakeClient{})) cmd.SetArgs([]string{"' '"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), `invalid stack name: "' '"`) } diff --git a/cli/command/stack/list_test.go b/cli/command/stack/list_test.go index f4c2d41b3c01..be76aa7e3da0 100644 --- a/cli/command/stack/list_test.go +++ b/cli/command/stack/list_test.go @@ -25,18 +25,21 @@ func TestListErrors(t *testing.T) { expectedError: "accepts no argument", }, { + args: []string{}, flags: map[string]string{ "format": "{{invalid format}}", }, expectedError: "template parsing error", }, { + args: []string{}, serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { return []swarm.Service{}, errors.Errorf("error getting services") }, expectedError: "error getting services", }, { + args: []string{}, serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { return []swarm.Service{*builders.Service()}, nil }, @@ -45,15 +48,19 @@ func TestListErrors(t *testing.T) { } for _, tc := range testCases { - cmd := newListCommand(test.NewFakeCli(&fakeClient{ - serviceListFunc: tc.serviceListFunc, - })) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - for key, value := range tc.flags { - assert.Check(t, cmd.Flags().Set(key, value)) - } - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.expectedError, func(t *testing.T) { + cmd := newListCommand(test.NewFakeCli(&fakeClient{ + serviceListFunc: tc.serviceListFunc, + })) + cmd.SetArgs(tc.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + for key, value := range tc.flags { + assert.Check(t, cmd.Flags().Set(key, value)) + } + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } @@ -97,6 +104,7 @@ func TestStackList(t *testing.T) { } for _, tc := range testCases { + tc := tc t.Run(tc.doc, func(t *testing.T) { var services []swarm.Service for _, name := range tc.serviceNames { diff --git a/cli/command/stack/loader/loader.go b/cli/command/stack/loader/loader.go index 7521d0471ded..5608a81c8fa3 100644 --- a/cli/command/stack/loader/loader.go +++ b/cli/command/stack/loader/loader.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package loader diff --git a/cli/command/stack/ps_test.go b/cli/command/stack/ps_test.go index a6b87393608a..e14002e210a6 100644 --- a/cli/command/stack/ps_test.go +++ b/cli/command/stack/ps_test.go @@ -40,12 +40,16 @@ func TestStackPsErrors(t *testing.T) { } for _, tc := range testCases { - cmd := newPsCommand(test.NewFakeCli(&fakeClient{ - taskListFunc: tc.taskListFunc, - })) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.expectedError, func(t *testing.T) { + cmd := newPsCommand(test.NewFakeCli(&fakeClient{ + taskListFunc: tc.taskListFunc, + })) + cmd.SetArgs(tc.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } @@ -156,6 +160,7 @@ func TestStackPs(t *testing.T) { } for _, tc := range testCases { + tc := tc t.Run(tc.doc, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ taskListFunc: tc.taskListFunc, @@ -169,6 +174,7 @@ func TestStackPs(t *testing.T) { assert.Check(t, cmd.Flags().Set(key, value)) } cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) if tc.expectedErr != "" { assert.Error(t, cmd.Execute(), tc.expectedErr) diff --git a/cli/command/stack/remove_test.go b/cli/command/stack/remove_test.go index 0c4bd2a8e34a..ed5c9e4f8b26 100644 --- a/cli/command/stack/remove_test.go +++ b/cli/command/stack/remove_test.go @@ -45,6 +45,7 @@ func TestRemoveWithEmptyName(t *testing.T) { cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{})) cmd.SetArgs([]string{"good", "' '", "alsogood"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), `invalid stack name: "' '"`) } @@ -157,6 +158,7 @@ func TestRemoveContinueAfterError(t *testing.T) { } cmd := newRemoveCommand(test.NewFakeCli(cli)) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) cmd.SetArgs([]string{"foo", "bar"}) assert.Error(t, cmd.Execute(), "Failed to remove some resources from stack: foo") diff --git a/cli/command/stack/services_test.go b/cli/command/stack/services_test.go index 2ec6431558a0..8d24a4e49ea0 100644 --- a/cli/command/stack/services_test.go +++ b/cli/command/stack/services_test.go @@ -80,6 +80,7 @@ func TestStackServicesErrors(t *testing.T) { assert.Check(t, cmd.Flags().Set(key, value)) } cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) }) } @@ -89,6 +90,7 @@ func TestRunServicesWithEmptyName(t *testing.T) { cmd := newServicesCommand(test.NewFakeCli(&fakeClient{})) cmd.SetArgs([]string{"' '"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), `invalid stack name: "' '"`) } diff --git a/cli/command/stack/swarm/remove.go b/cli/command/stack/swarm/remove.go index cde317fe0a66..5641ea24bdf6 100644 --- a/cli/command/stack/swarm/remove.go +++ b/cli/command/stack/swarm/remove.go @@ -48,7 +48,7 @@ func RunRemove(ctx context.Context, dockerCli command.Cli, opts options.Remove) } if len(services)+len(networks)+len(secrets)+len(configs) == 0 { - fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", namespace) + _, _ = fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", namespace) continue } @@ -71,7 +71,7 @@ func RunRemove(ctx context.Context, dockerCli command.Cli, opts options.Remove) } if len(errs) > 0 { - return errors.Errorf(strings.Join(errs, "\n")) + return errors.New(strings.Join(errs, "\n")) } return nil } diff --git a/cli/command/swarm/ca_test.go b/cli/command/swarm/ca_test.go index 29d2cd359ad4..3096079e1757 100644 --- a/cli/command/swarm/ca_test.go +++ b/cli/command/swarm/ca_test.go @@ -145,6 +145,7 @@ func TestDisplayTrustRootInvalidFlags(t *testing.T) { })) assert.Check(t, cmd.Flags().Parse(testCase.args)) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), testCase.errorMsg) } } diff --git a/cli/command/swarm/init_test.go b/cli/command/swarm/init_test.go index e2153040e998..fbed83bde8eb 100644 --- a/cli/command/swarm/init_test.go +++ b/cli/command/swarm/init_test.go @@ -63,18 +63,22 @@ func TestSwarmInitErrorOnAPIFailure(t *testing.T) { }, } for _, tc := range testCases { - cmd := newInitCommand( - test.NewFakeCli(&fakeClient{ - swarmInitFunc: tc.swarmInitFunc, - swarmInspectFunc: tc.swarmInspectFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, - nodeInspectFunc: tc.nodeInspectFunc, - })) - for key, value := range tc.flags { - cmd.Flags().Set(key, value) - } - cmd.SetOut(io.Discard) - assert.Error(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cmd := newInitCommand( + test.NewFakeCli(&fakeClient{ + swarmInitFunc: tc.swarmInitFunc, + swarmInspectFunc: tc.swarmInspectFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + nodeInspectFunc: tc.nodeInspectFunc, + })) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + }) } } diff --git a/cli/command/swarm/join_test.go b/cli/command/swarm/join_test.go index 5ed5e0a29f8f..9a60e9e25e28 100644 --- a/cli/command/swarm/join_test.go +++ b/cli/command/swarm/join_test.go @@ -48,14 +48,18 @@ func TestSwarmJoinErrors(t *testing.T) { }, } for _, tc := range testCases { - cmd := newJoinCommand( - test.NewFakeCli(&fakeClient{ - swarmJoinFunc: tc.swarmJoinFunc, - infoFunc: tc.infoFunc, - })) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cmd := newJoinCommand( + test.NewFakeCli(&fakeClient{ + swarmJoinFunc: tc.swarmJoinFunc, + infoFunc: tc.infoFunc, + })) + cmd.SetArgs(tc.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } @@ -89,12 +93,15 @@ func TestSwarmJoin(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{ - infoFunc: tc.infoFunc, + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + infoFunc: tc.infoFunc, + }) + cmd := newJoinCommand(cli) + cmd.SetArgs([]string{"remote"}) + assert.NilError(t, cmd.Execute()) + assert.Check(t, is.Equal(strings.TrimSpace(cli.OutBuffer().String()), tc.expected)) }) - cmd := newJoinCommand(cli) - cmd.SetArgs([]string{"remote"}) - assert.NilError(t, cmd.Execute()) - assert.Check(t, is.Equal(strings.TrimSpace(cli.OutBuffer().String()), tc.expected)) } } diff --git a/cli/command/swarm/join_token_test.go b/cli/command/swarm/join_token_test.go index 3d1bf18de643..21b89e66529b 100644 --- a/cli/command/swarm/join_token_test.go +++ b/cli/command/swarm/join_token_test.go @@ -87,19 +87,23 @@ func TestSwarmJoinTokenErrors(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - swarmUpdateFunc: tc.swarmUpdateFunc, - infoFunc: tc.infoFunc, - nodeInspectFunc: tc.nodeInspectFunc, + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + swarmUpdateFunc: tc.swarmUpdateFunc, + infoFunc: tc.infoFunc, + nodeInspectFunc: tc.nodeInspectFunc, + }) + cmd := newJoinTokenCommand(cli) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + assert.Check(t, cmd.Flags().Set(key, value)) + } + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) }) - cmd := newJoinTokenCommand(cli) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - assert.Check(t, cmd.Flags().Set(key, value)) - } - cmd.SetOut(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } @@ -194,17 +198,20 @@ func TestSwarmJoinToken(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - infoFunc: tc.infoFunc, - nodeInspectFunc: tc.nodeInspectFunc, + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + infoFunc: tc.infoFunc, + nodeInspectFunc: tc.nodeInspectFunc, + }) + cmd := newJoinTokenCommand(cli) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + assert.Check(t, cmd.Flags().Set(key, value)) + } + assert.NilError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("jointoken-%s.golden", tc.name)) }) - cmd := newJoinTokenCommand(cli) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - assert.Check(t, cmd.Flags().Set(key, value)) - } - assert.NilError(t, cmd.Execute()) - golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("jointoken-%s.golden", tc.name)) } } diff --git a/cli/command/swarm/leave_test.go b/cli/command/swarm/leave_test.go index e1005828d067..58192e5ab470 100644 --- a/cli/command/swarm/leave_test.go +++ b/cli/command/swarm/leave_test.go @@ -32,13 +32,17 @@ func TestSwarmLeaveErrors(t *testing.T) { }, } for _, tc := range testCases { - cmd := newLeaveCommand( - test.NewFakeCli(&fakeClient{ - swarmLeaveFunc: tc.swarmLeaveFunc, - })) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cmd := newLeaveCommand( + test.NewFakeCli(&fakeClient{ + swarmLeaveFunc: tc.swarmLeaveFunc, + })) + cmd.SetArgs(tc.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } diff --git a/cli/command/swarm/unlock_key_test.go b/cli/command/swarm/unlock_key_test.go index cfe49feb3e47..15d7f2e84022 100644 --- a/cli/command/swarm/unlock_key_test.go +++ b/cli/command/swarm/unlock_key_test.go @@ -80,18 +80,22 @@ func TestSwarmUnlockKeyErrors(t *testing.T) { }, } for _, tc := range testCases { - cmd := newUnlockKeyCommand( - test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - swarmUpdateFunc: tc.swarmUpdateFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, - })) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - assert.Check(t, cmd.Flags().Set(key, value)) - } - cmd.SetOut(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cmd := newUnlockKeyCommand( + test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + swarmUpdateFunc: tc.swarmUpdateFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + })) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + assert.Check(t, cmd.Flags().Set(key, value)) + } + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } @@ -154,17 +158,20 @@ func TestSwarmUnlockKey(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - swarmUpdateFunc: tc.swarmUpdateFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + swarmUpdateFunc: tc.swarmUpdateFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + }) + cmd := newUnlockKeyCommand(cli) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + assert.Check(t, cmd.Flags().Set(key, value)) + } + assert.NilError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("unlockkeys-%s.golden", tc.name)) }) - cmd := newUnlockKeyCommand(cli) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - assert.Check(t, cmd.Flags().Set(key, value)) - } - assert.NilError(t, cmd.Execute()) - golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("unlockkeys-%s.golden", tc.name)) } } diff --git a/cli/command/swarm/unlock_test.go b/cli/command/swarm/unlock_test.go index c5d533ea9e84..04a996acb518 100644 --- a/cli/command/swarm/unlock_test.go +++ b/cli/command/swarm/unlock_test.go @@ -64,14 +64,18 @@ func TestSwarmUnlockErrors(t *testing.T) { }, } for _, tc := range testCases { - cmd := newUnlockCommand( - test.NewFakeCli(&fakeClient{ - infoFunc: tc.infoFunc, - swarmUnlockFunc: tc.swarmUnlockFunc, - })) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cmd := newUnlockCommand( + test.NewFakeCli(&fakeClient{ + infoFunc: tc.infoFunc, + swarmUnlockFunc: tc.swarmUnlockFunc, + })) + cmd.SetArgs(tc.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } diff --git a/cli/command/swarm/update_test.go b/cli/command/swarm/update_test.go index 29432cd55b6e..debab12d2165 100644 --- a/cli/command/swarm/update_test.go +++ b/cli/command/swarm/update_test.go @@ -65,18 +65,22 @@ func TestSwarmUpdateErrors(t *testing.T) { }, } for _, tc := range testCases { - cmd := newUpdateCommand( - test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - swarmUpdateFunc: tc.swarmUpdateFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, - })) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - assert.Check(t, cmd.Flags().Set(key, value)) - } - cmd.SetOut(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cmd := newUpdateCommand( + test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + swarmUpdateFunc: tc.swarmUpdateFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + })) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + assert.Check(t, cmd.Flags().Set(key, value)) + } + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } @@ -165,18 +169,25 @@ func TestSwarmUpdate(t *testing.T) { }, } for _, tc := range testCases { - cli := test.NewFakeCli(&fakeClient{ - swarmInspectFunc: tc.swarmInspectFunc, - swarmUpdateFunc: tc.swarmUpdateFunc, - swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + swarmUpdateFunc: tc.swarmUpdateFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + }) + cmd := newUpdateCommand(cli) + if tc.args == nil { + cmd.SetArgs([]string{}) + } else { + cmd.SetArgs(tc.args) + } + for key, value := range tc.flags { + assert.Check(t, cmd.Flags().Set(key, value)) + } + cmd.SetOut(cli.OutBuffer()) + assert.NilError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("update-%s.golden", tc.name)) }) - cmd := newUpdateCommand(cli) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - assert.Check(t, cmd.Flags().Set(key, value)) - } - cmd.SetOut(cli.OutBuffer()) - assert.NilError(t, cmd.Execute()) - golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("update-%s.golden", tc.name)) } } diff --git a/cli/command/system/client_test.go b/cli/command/system/client_test.go index b6eeb3bd9294..e115d61a3be5 100644 --- a/cli/command/system/client_test.go +++ b/cli/command/system/client_test.go @@ -7,7 +7,11 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/events" "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/swarm" + "github.com/docker/docker/api/types/system" + "github.com/docker/docker/api/types/volume" "github.com/docker/docker/client" ) @@ -15,22 +19,27 @@ type fakeClient struct { client.Client version string - serverVersion func(ctx context.Context) (types.Version, error) - eventsFn func(context.Context, events.ListOptions) (<-chan events.Message, <-chan error) + containerListFunc func(context.Context, container.ListOptions) ([]types.Container, 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) -} - -func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) { - return cli.serverVersion(ctx) + nodeListFunc func(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) + serverVersion func(ctx context.Context) (types.Version, error) + volumeListFunc func(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) } func (cli *fakeClient) ClientVersion() string { return cli.version } -func (cli *fakeClient) Events(ctx context.Context, opts events.ListOptions) (<-chan events.Message, <-chan error) { - return cli.eventsFn(ctx, opts) +func (cli *fakeClient) ContainerList(ctx context.Context, options container.ListOptions) ([]types.Container, error) { + if cli.containerListFunc != nil { + return cli.containerListFunc(ctx, options) + } + return []types.Container{}, nil } func (cli *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) { @@ -40,9 +49,52 @@ func (cli *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters 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) { if cli.networkPruneFunc != nil { return cli.networkPruneFunc(ctx, pruneFilter) } 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 +} diff --git a/cli/command/system/completion.go b/cli/command/system/completion.go new file mode 100644 index 000000000000..0f70d1a8bf5c --- /dev/null +++ b/cli/command/system/completion.go @@ -0,0 +1,237 @@ +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 +} diff --git a/cli/command/system/completion_test.go b/cli/command/system/completion_test.go new file mode 100644 index 000000000000..b0631c8a57a8 --- /dev/null +++ b/cli/command/system/completion_test.go @@ -0,0 +1,165 @@ +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) ([]types.Container, error) { + return []types.Container{ + *builders.Container("c1"), + *builders.Container("c2"), + }, nil + }, + }, + toComplete: "container=", + expected: []string{"container=c1", "container=c2"}, + }, + { + client: &fakeClient{ + containerListFunc: func(_ context.Context, _ container.ListOptions) ([]types.Container, 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)) + } +} diff --git a/cli/command/system/events.go b/cli/command/system/events.go index a54d30ffed38..9b5f965e5b3c 100644 --- a/cli/command/system/events.go +++ b/cli/command/system/events.go @@ -50,6 +50,8 @@ func NewEventsCommand(dockerCli command.Cli) *cobra.Command { 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. + _ = cmd.RegisterFlagCompletionFunc("filter", completeEventFilters(dockerCli)) + return cmd } diff --git a/cli/command/system/info.go b/cli/command/system/info.go index aaf597cf897b..108a65a5f1f4 100644 --- a/cli/command/system/info.go +++ b/cli/command/system/info.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package system @@ -273,21 +273,9 @@ func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) []error { if info.OSType == "linux" { fprintln(output, " Init Binary:", info.InitBinary) - - for _, ci := range []struct { - Name string - Commit system.Commit - }{ - {"containerd", info.ContainerdCommit}, - {"runc", info.RuncCommit}, - {"init", info.InitCommit}, - } { - fprintf(output, " %s version: %s", ci.Name, ci.Commit.ID) - if ci.Commit.ID != ci.Commit.Expected { - fprintf(output, " (expected: %s)", ci.Commit.Expected) - } - fprintln(output) - } + fprintln(output, " containerd version:", info.ContainerdCommit.ID) + fprintln(output, " runc version:", info.RuncCommit.ID) + fprintln(output, " init version:", info.InitCommit.ID) if len(info.SecurityOptions) != 0 { if kvs, err := system.DecodeSecurityOptions(info.SecurityOptions); err != nil { errs = append(errs, err) @@ -372,7 +360,7 @@ func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) []error { fprintln(output, " Product License:", info.ProductLicense) } - if info.DefaultAddressPools != nil && len(info.DefaultAddressPools) > 0 { + if len(info.DefaultAddressPools) > 0 { fprintln(output, " Default Address Pools:") for _, pool := range info.DefaultAddressPools { fprintf(output, " Base: %s, Size: %d\n", pool.Base, pool.Size) diff --git a/cli/command/system/info_test.go b/cli/command/system/info_test.go index 616256c4cb9c..6493393c07e0 100644 --- a/cli/command/system/info_test.go +++ b/cli/command/system/info_test.go @@ -53,8 +53,6 @@ var sampleInfoNoSwarm = system.Info{ CPUShares: true, CPUSet: true, IPv4Forwarding: true, - BridgeNfIptables: true, - BridgeNfIP6tables: true, Debug: true, NFd: 33, OomKillDisable: true, @@ -242,8 +240,6 @@ func TestPrettyPrintInfo(t *testing.T) { infoWithWarningsLinux.CPUShares = false infoWithWarningsLinux.CPUSet = false infoWithWarningsLinux.IPv4Forwarding = false - infoWithWarningsLinux.BridgeNfIptables = false - infoWithWarningsLinux.BridgeNfIP6tables = false sampleInfoDaemonWarnings := sampleInfoNoSwarm sampleInfoDaemonWarnings.Warnings = []string{ @@ -255,8 +251,6 @@ func TestPrettyPrintInfo(t *testing.T) { "WARNING: No cpu shares support", "WARNING: No cpuset support", "WARNING: IPv4 forwarding is disabled", - "WARNING: bridge-nf-call-iptables is disabled", - "WARNING: bridge-nf-call-ip6tables is disabled", } sampleInfoBadSecurity := sampleInfoNoSwarm diff --git a/cli/command/system/inspect.go b/cli/command/system/inspect.go index d4aed789b17c..9093400d0ec9 100644 --- a/cli/command/system/inspect.go +++ b/cli/command/system/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package system diff --git a/cli/command/system/prune_test.go b/cli/command/system/prune_test.go index 8dadc4c8633d..fa7294467f21 100644 --- a/cli/command/system/prune_test.go +++ b/cli/command/system/prune_test.go @@ -2,6 +2,7 @@ package system import ( "context" + "io" "testing" "github.com/docker/cli/cli/config/configfile" @@ -18,6 +19,8 @@ func TestPrunePromptPre131DoesNotIncludeBuildCache(t *testing.T) { cli := test.NewFakeCli(&fakeClient{version: "1.30"}) cmd := newPruneCommand(cli) cmd.SetArgs([]string{}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), "system prune has been cancelled") expected := `WARNING! This will remove: - all stopped containers @@ -35,6 +38,8 @@ func TestPrunePromptFilters(t *testing.T) { }) cmd := newPruneCommand(cli) cmd.SetArgs([]string{"--filter", "until=24h", "--filter", "label=hello-world", "--filter", "label!=foo=bar", "--filter", "label=bar=baz"}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), "system prune has been cancelled") expected := `WARNING! This will remove: @@ -69,5 +74,8 @@ func TestSystemPrunePromptTermination(t *testing.T) { }) cmd := newPruneCommand(cli) + cmd.SetArgs([]string{}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) test.TerminatePrompt(ctx, t, cmd, cli) } diff --git a/cli/command/system/testdata/docker-info-badsec.json.golden b/cli/command/system/testdata/docker-info-badsec.json.golden index f25be73ccc13..bf3263e72b9e 100644 --- a/cli/command/system/testdata/docker-info-badsec.json.golden +++ b/cli/command/system/testdata/docker-info-badsec.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"BridgeNfIptables":true,"BridgeNfIp6tables":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["foo="],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ServerErrors":["a server error occurred"],"ClientInfo":{"Debug":false,"Context":"","Plugins":[],"Warnings":null}} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["foo="],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ServerErrors":["a server error occurred"],"ClientInfo":{"Debug":false,"Context":"","Plugins":[],"Warnings":null}} diff --git a/cli/command/system/testdata/docker-info-daemon-warnings.json.golden b/cli/command/system/testdata/docker-info-daemon-warnings.json.golden index 78caf6cd9ea9..71cd3490d285 100644 --- a/cli/command/system/testdata/docker-info-daemon-warnings.json.golden +++ b/cli/command/system/testdata/docker-info-daemon-warnings.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"BridgeNfIptables":true,"BridgeNfIp6tables":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":["WARNING: No memory limit support","WARNING: No swap limit support","WARNING: No oom kill disable support","WARNING: No cpu cfs quota support","WARNING: No cpu cfs period support","WARNING: No cpu shares support","WARNING: No cpuset support","WARNING: IPv4 forwarding is disabled","WARNING: bridge-nf-call-iptables is disabled","WARNING: bridge-nf-call-ip6tables is disabled"],"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":["WARNING: No memory limit support","WARNING: No swap limit support","WARNING: No oom kill disable support","WARNING: No cpu cfs quota support","WARNING: No cpu cfs period support","WARNING: No cpu shares support","WARNING: No cpuset support","WARNING: IPv4 forwarding is disabled"],"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}} diff --git a/cli/command/system/testdata/docker-info-no-swarm.json.golden b/cli/command/system/testdata/docker-info-no-swarm.json.golden index 32f15edf00df..c40f87c6a516 100644 --- a/cli/command/system/testdata/docker-info-no-swarm.json.golden +++ b/cli/command/system/testdata/docker-info-no-swarm.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"BridgeNfIptables":true,"BridgeNfIp6tables":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}} diff --git a/cli/command/system/testdata/docker-info-plugins.json.golden b/cli/command/system/testdata/docker-info-plugins.json.golden index 539bc0b01cb6..d0038bf6e028 100644 --- a/cli/command/system/testdata/docker-info-plugins.json.golden +++ b/cli/command/system/testdata/docker-info-plugins.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"BridgeNfIptables":true,"BridgeNfIp6tables":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":false,"Context":"default","Plugins":[{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","Version":"0.1.0","ShortDescription":"unit test is good","Name":"goodplugin","Path":"/path/to/docker-goodplugin"},{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","ShortDescription":"this plugin has no version","Name":"unversionedplugin","Path":"/path/to/docker-unversionedplugin"},{"Name":"badplugin","Path":"/path/to/docker-badplugin","Err":"something wrong"}],"Warnings":null}} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":false,"Context":"default","Plugins":[{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","Version":"0.1.0","ShortDescription":"unit test is good","Name":"goodplugin","Path":"/path/to/docker-goodplugin"},{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","ShortDescription":"this plugin has no version","Name":"unversionedplugin","Path":"/path/to/docker-unversionedplugin"},{"Name":"badplugin","Path":"/path/to/docker-badplugin","Err":"something wrong"}],"Warnings":null}} diff --git a/cli/command/system/testdata/docker-info-warnings.golden b/cli/command/system/testdata/docker-info-warnings.golden index 579a8d422a9f..508a8584d277 100644 --- a/cli/command/system/testdata/docker-info-warnings.golden +++ b/cli/command/system/testdata/docker-info-warnings.golden @@ -6,5 +6,3 @@ WARNING: No cpu cfs period support WARNING: No cpu shares support WARNING: No cpuset support WARNING: IPv4 forwarding is disabled -WARNING: bridge-nf-call-iptables is disabled -WARNING: bridge-nf-call-ip6tables is disabled diff --git a/cli/command/system/testdata/docker-info-with-swarm.json.golden b/cli/command/system/testdata/docker-info-with-swarm.json.golden index bba05f5e1bb8..0a5f81fe7e60 100644 --- a/cli/command/system/testdata/docker-info-with-swarm.json.golden +++ b/cli/command/system/testdata/docker-info-with-swarm.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"BridgeNfIptables":true,"BridgeNfIp6tables":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"qo2dfdig9mmxqkawulggepdih","NodeAddr":"165.227.107.89","LocalNodeState":"active","ControlAvailable":true,"Error":"","RemoteManagers":[{"NodeID":"qo2dfdig9mmxqkawulggepdih","Addr":"165.227.107.89:2377"}],"Nodes":1,"Managers":1,"Cluster":{"ID":"9vs5ygs0gguyyec4iqf2314c0","Version":{"Index":11},"CreatedAt":"2017-08-24T17:34:19.278062352Z","UpdatedAt":"2017-08-24T17:34:42.398815481Z","Spec":{"Name":"default","Labels":null,"Orchestration":{"TaskHistoryRetentionLimit":5},"Raft":{"SnapshotInterval":10000,"KeepOldSnapshots":0,"LogEntriesForSlowFollowers":500,"ElectionTick":3,"HeartbeatTick":1},"Dispatcher":{"HeartbeatPeriod":5000000000},"CAConfig":{"NodeCertExpiry":7776000000000000},"TaskDefaults":{},"EncryptionConfig":{"AutoLockManagers":true}},"TLSInfo":{"TrustRoot":"\n-----BEGIN CERTIFICATE-----\nMIIBajCCARCgAwIBAgIUaFCW5xsq8eyiJ+Pmcv3MCflMLnMwCgYIKoZIzj0EAwIw\nEzERMA8GA1UEAxMIc3dhcm0tY2EwHhcNMTcwODI0MTcyOTAwWhcNMzcwODE5MTcy\nOTAwWjATMREwDwYDVQQDEwhzd2FybS1jYTBZMBMGByqGSM49AgEGCCqGSM49AwEH\nA0IABDy7NebyUJyUjWJDBUdnZoV6GBxEGKO4TZPNDwnxDxJcUdLVaB7WGa4/DLrW\nUfsVgh1JGik2VTiLuTMA1tLlNPOjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB\nAf8EBTADAQH/MB0GA1UdDgQWBBQl16XFtaaXiUAwEuJptJlDjfKskDAKBggqhkjO\nPQQDAgNIADBFAiEAo9fTQNM5DP9bHVcTJYfl2Cay1bFu1E+lnpmN+EYJfeACIGKH\n1pCUkZ+D0IB6CiEZGWSHyLuXPM1rlP+I5KuS7sB8\n-----END CERTIFICATE-----\n","CertIssuerSubject":"MBMxETAPBgNVBAMTCHN3YXJtLWNh","CertIssuerPublicKey":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPLs15vJQnJSNYkMFR2dmhXoYHEQYo7hNk80PCfEPElxR0tVoHtYZrj8MutZR+xWCHUkaKTZVOIu5MwDW0uU08w=="},"RootRotationInProgress":false,"DefaultAddrPool":null,"SubnetSize":0,"DataPathPort":0}},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":false,"Context":"default","Plugins":[],"Warnings":null}} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"qo2dfdig9mmxqkawulggepdih","NodeAddr":"165.227.107.89","LocalNodeState":"active","ControlAvailable":true,"Error":"","RemoteManagers":[{"NodeID":"qo2dfdig9mmxqkawulggepdih","Addr":"165.227.107.89:2377"}],"Nodes":1,"Managers":1,"Cluster":{"ID":"9vs5ygs0gguyyec4iqf2314c0","Version":{"Index":11},"CreatedAt":"2017-08-24T17:34:19.278062352Z","UpdatedAt":"2017-08-24T17:34:42.398815481Z","Spec":{"Name":"default","Labels":null,"Orchestration":{"TaskHistoryRetentionLimit":5},"Raft":{"SnapshotInterval":10000,"KeepOldSnapshots":0,"LogEntriesForSlowFollowers":500,"ElectionTick":3,"HeartbeatTick":1},"Dispatcher":{"HeartbeatPeriod":5000000000},"CAConfig":{"NodeCertExpiry":7776000000000000},"TaskDefaults":{},"EncryptionConfig":{"AutoLockManagers":true}},"TLSInfo":{"TrustRoot":"\n-----BEGIN CERTIFICATE-----\nMIIBajCCARCgAwIBAgIUaFCW5xsq8eyiJ+Pmcv3MCflMLnMwCgYIKoZIzj0EAwIw\nEzERMA8GA1UEAxMIc3dhcm0tY2EwHhcNMTcwODI0MTcyOTAwWhcNMzcwODE5MTcy\nOTAwWjATMREwDwYDVQQDEwhzd2FybS1jYTBZMBMGByqGSM49AgEGCCqGSM49AwEH\nA0IABDy7NebyUJyUjWJDBUdnZoV6GBxEGKO4TZPNDwnxDxJcUdLVaB7WGa4/DLrW\nUfsVgh1JGik2VTiLuTMA1tLlNPOjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB\nAf8EBTADAQH/MB0GA1UdDgQWBBQl16XFtaaXiUAwEuJptJlDjfKskDAKBggqhkjO\nPQQDAgNIADBFAiEAo9fTQNM5DP9bHVcTJYfl2Cay1bFu1E+lnpmN+EYJfeACIGKH\n1pCUkZ+D0IB6CiEZGWSHyLuXPM1rlP+I5KuS7sB8\n-----END CERTIFICATE-----\n","CertIssuerSubject":"MBMxETAPBgNVBAMTCHN3YXJtLWNh","CertIssuerPublicKey":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPLs15vJQnJSNYkMFR2dmhXoYHEQYo7hNk80PCfEPElxR0tVoHtYZrj8MutZR+xWCHUkaKTZVOIu5MwDW0uU08w=="},"RootRotationInProgress":false,"DefaultAddrPool":null,"SubnetSize":0,"DataPathPort":0}},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":false,"Context":"default","Plugins":[],"Warnings":null}} diff --git a/cli/command/system/version_test.go b/cli/command/system/version_test.go index c0cf6425975f..6f8b243de4b4 100644 --- a/cli/command/system/version_test.go +++ b/cli/command/system/version_test.go @@ -3,6 +3,7 @@ package system import ( "context" "errors" + "io" "strings" "testing" @@ -20,7 +21,9 @@ func TestVersionWithoutServer(t *testing.T) { }, }) cmd := NewVersionCommand(cli) + cmd.SetArgs([]string{}) cmd.SetOut(cli.Err()) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), "no server") out := cli.OutBuffer().String() // TODO: use an assertion like e2e/image/build_test.go:assertBuildOutput() diff --git a/cli/command/telemetry_docker.go b/cli/command/telemetry_docker.go index 94ab3a3929dd..298209e2ba12 100644 --- a/cli/command/telemetry_docker.go +++ b/cli/command/telemetry_docker.go @@ -1,13 +1,18 @@ -// FIXME(jsternberg): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.22 package command import ( "context" + "fmt" + "io/fs" "net/url" "os" "path" + "path/filepath" + "strings" + "unicode" "github.com/pkg/errors" "go.opentelemetry.io/otel" @@ -77,14 +82,7 @@ func dockerExporterOTLPEndpoint(cli Cli) (endpoint string, secure bool) { switch u.Scheme { case "unix": - // Unix sockets are a bit weird. OTEL seems to imply they - // can be used as an environment variable and are handled properly, - // but they don't seem to be as the behavior of the environment variable - // is to strip the scheme from the endpoint, but the underlying implementation - // needs the scheme to use the correct resolver. - // - // We'll just handle this in a special way and add the unix:// back to the endpoint. - endpoint = "unix://" + path.Join(u.Host, u.Path) + endpoint = unixSocketEndpoint(u) case "https": secure = true fallthrough @@ -135,3 +133,109 @@ func dockerMetricExporter(ctx context.Context, cli Cli) []sdkmetric.Option { } return []sdkmetric.Option{sdkmetric.WithReader(newCLIReader(exp))} } + +// unixSocketEndpoint converts the unix scheme from URL to +// an OTEL endpoint that can be used with the OTLP exporter. +// +// The OTLP exporter handles unix sockets in a strange way. +// It seems to imply they can be used as an environment variable +// and are handled properly, but they don't seem to be as the behavior +// of the environment variable is to strip the scheme from the endpoint +// while the underlying implementation needs the scheme to use the +// correct resolver. +func unixSocketEndpoint(u *url.URL) string { + // GRPC does not allow host to be used. + socketPath := u.Path + + // If we are on windows and we have an absolute path + // that references a letter drive, check to see if the + // WSL equivalent path exists and we should use that instead. + if isWsl() { + if p := wslSocketPath(socketPath, os.DirFS("/")); p != "" { + socketPath = p + } + } + // Enforce that we are using forward slashes. + return "unix://" + filepath.ToSlash(socketPath) +} + +// wslSocketPath will convert the referenced URL to a WSL-compatible +// path and check if that path exists. If the path exists, it will +// be returned. +func wslSocketPath(s string, f fs.FS) string { + if p := toWslPath(s); p != "" { + if _, err := stat(p, f); err == nil { + return "/" + p + } + } + return "" +} + +// toWslPath converts the referenced URL to a WSL-compatible +// path if this looks like a Windows absolute path. +// +// If no drive is in the URL, defaults to the C drive. +func toWslPath(s string) string { + drive, p, ok := parseUNCPath(s) + if !ok { + return "" + } + return fmt.Sprintf("mnt/%s%s", strings.ToLower(drive), p) +} + +func parseUNCPath(s string) (drive, p string, ok bool) { + // UNC paths use backslashes but we're using forward slashes + // so also enforce that here. + // + // In reality, this should have been enforced much earlier + // than here since backslashes aren't allowed in URLs, but + // we're going to code defensively here. + s = filepath.ToSlash(s) + + const uncPrefix = "//./" + if !strings.HasPrefix(s, uncPrefix) { + // Not a UNC path. + return "", "", false + } + s = s[len(uncPrefix):] + + parts := strings.SplitN(s, "/", 2) + if len(parts) != 2 { + // Not enough components. + return "", "", false + } + + drive, ok = splitWindowsDrive(parts[0]) + if !ok { + // Not a windows drive. + return "", "", false + } + return drive, "/" + parts[1], true +} + +// splitWindowsDrive checks if the string references a windows +// drive (such as c:) and returns the drive letter if it is. +func splitWindowsDrive(s string) (string, bool) { + if b := []rune(s); len(b) == 2 && unicode.IsLetter(b[0]) && b[1] == ':' { + return string(b[0]), true + } + return "", false +} + +func stat(p string, f fs.FS) (fs.FileInfo, error) { + if f, ok := f.(fs.StatFS); ok { + return f.Stat(p) + } + + file, err := f.Open(p) + if err != nil { + return nil, err + } + + defer file.Close() + return file.Stat() +} + +func isWsl() bool { + return os.Getenv("WSL_DISTRO_NAME") != "" +} diff --git a/cli/command/telemetry_docker_test.go b/cli/command/telemetry_docker_test.go new file mode 100644 index 000000000000..bb0ba7c22329 --- /dev/null +++ b/cli/command/telemetry_docker_test.go @@ -0,0 +1,57 @@ +package command + +import ( + "io/fs" + "net/url" + "testing" + "testing/fstest" + + "gotest.tools/v3/assert" +) + +func TestWslSocketPath(t *testing.T) { + testCases := []struct { + doc string + fs fs.FS + url string + expected string + }{ + { + doc: "filesystem where WSL path does not exist", + fs: fstest.MapFS{ + "my/file/path": {}, + }, + url: "unix:////./c:/my/file/path", + expected: "", + }, + { + doc: "filesystem where WSL path exists", + fs: fstest.MapFS{ + "mnt/c/my/file/path": {}, + }, + url: "unix:////./c:/my/file/path", + expected: "/mnt/c/my/file/path", + }, + { + doc: "filesystem where WSL path exists uppercase URL", + fs: fstest.MapFS{ + "mnt/c/my/file/path": {}, + }, + url: "unix:////./C:/my/file/path", + expected: "/mnt/c/my/file/path", + }, + } + + for _, tc := range testCases { + t.Run(tc.doc, func(t *testing.T) { + u, err := url.Parse(tc.url) + assert.NilError(t, err) + // Ensure host is empty. + assert.Equal(t, u.Host, "") + + result := wslSocketPath(u.Path, tc.fs) + + assert.Equal(t, result, tc.expected) + }) + } +} diff --git a/cli/command/telemetry_utils.go b/cli/command/telemetry_utils.go index 905f8a4614c0..680415b63770 100644 --- a/cli/command/telemetry_utils.go +++ b/cli/command/telemetry_utils.go @@ -9,6 +9,7 @@ import ( "github.com/docker/cli/cli/version" "github.com/pkg/errors" "github.com/spf13/cobra" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" ) @@ -94,7 +95,9 @@ func startCobraCommandTimer(mp metric.MeterProvider, attrs []attribute.KeyValue) metric.WithAttributes(cmdStatusAttrs...), ) if mp, ok := mp.(MeterProvider); ok { - mp.ForceFlush(ctx) + if err := mp.ForceFlush(ctx); err != nil { + otel.Handle(err) + } } } } diff --git a/cli/command/trust/inspect.go b/cli/command/trust/inspect.go index 3de90b4e6f8e..6de786623427 100644 --- a/cli/command/trust/inspect.go +++ b/cli/command/trust/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package trust diff --git a/cli/command/trust/inspect_pretty_test.go b/cli/command/trust/inspect_pretty_test.go index f564a5f7c631..c06629d344f2 100644 --- a/cli/command/trust/inspect_pretty_test.go +++ b/cli/command/trust/inspect_pretty_test.go @@ -67,6 +67,7 @@ func TestTrustInspectPrettyCommandErrors(t *testing.T) { test.NewFakeCli(&fakeClient{})) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) cmd.Flags().Set("pretty", "true") assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } @@ -79,6 +80,7 @@ func TestTrustInspectPrettyCommandOfflineErrors(t *testing.T) { cmd.Flags().Set("pretty", "true") cmd.SetArgs([]string{"nonexistent-reg-name.io/image"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), "no signatures or cannot access nonexistent-reg-name.io/image") cli = test.NewFakeCli(&fakeClient{}) @@ -87,6 +89,7 @@ func TestTrustInspectPrettyCommandOfflineErrors(t *testing.T) { cmd.Flags().Set("pretty", "true") cmd.SetArgs([]string{"nonexistent-reg-name.io/image:tag"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), "no signatures or cannot access nonexistent-reg-name.io/image") } @@ -97,6 +100,7 @@ func TestTrustInspectPrettyCommandUninitializedErrors(t *testing.T) { cmd.Flags().Set("pretty", "true") cmd.SetArgs([]string{"reg/unsigned-img"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), "no signatures or cannot access reg/unsigned-img") cli = test.NewFakeCli(&fakeClient{}) @@ -105,6 +109,7 @@ func TestTrustInspectPrettyCommandUninitializedErrors(t *testing.T) { cmd.Flags().Set("pretty", "true") cmd.SetArgs([]string{"reg/unsigned-img:tag"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), "no signatures or cannot access reg/unsigned-img:tag") } @@ -115,6 +120,7 @@ func TestTrustInspectPrettyCommandEmptyNotaryRepoErrors(t *testing.T) { cmd.Flags().Set("pretty", "true") cmd.SetArgs([]string{"reg/img:unsigned-tag"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.NilError(t, cmd.Execute()) assert.Check(t, is.Contains(cli.OutBuffer().String(), "No signatures for reg/img:unsigned-tag")) assert.Check(t, is.Contains(cli.OutBuffer().String(), "Administrative keys for reg/img")) @@ -125,6 +131,7 @@ func TestTrustInspectPrettyCommandEmptyNotaryRepoErrors(t *testing.T) { cmd.Flags().Set("pretty", "true") cmd.SetArgs([]string{"reg/img"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.NilError(t, cmd.Execute()) assert.Check(t, is.Contains(cli.OutBuffer().String(), "No signatures for reg/img")) assert.Check(t, is.Contains(cli.OutBuffer().String(), "Administrative keys for reg/img")) diff --git a/cli/command/trust/inspect_test.go b/cli/command/trust/inspect_test.go index 692c5a6737df..5823f329093f 100644 --- a/cli/command/trust/inspect_test.go +++ b/cli/command/trust/inspect_test.go @@ -39,6 +39,7 @@ func TestTrustInspectCommandErrors(t *testing.T) { cmd.Flags().Set("pretty", "true") cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } @@ -86,6 +87,7 @@ func TestTrustInspectCommandRepositoryErrors(t *testing.T) { cmd := newInspectCommand(cli) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.err) if tc.golden != "" { golden.Assert(t, cli.OutBuffer().String(), tc.golden) diff --git a/cli/command/trust/key_generate_test.go b/cli/command/trust/key_generate_test.go index 4645c9ae3334..9781a5338461 100644 --- a/cli/command/trust/key_generate_test.go +++ b/cli/command/trust/key_generate_test.go @@ -42,6 +42,7 @@ func TestTrustKeyGenerateErrors(t *testing.T) { cmd := newKeyGenerateCommand(cli) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } @@ -50,8 +51,8 @@ func TestGenerateKeySuccess(t *testing.T) { pubKeyCWD := t.TempDir() privKeyStorageDir := t.TempDir() - passwd := "password" - cannedPasswordRetriever := passphrase.ConstantRetriever(passwd) + const testPass = "password" + cannedPasswordRetriever := passphrase.ConstantRetriever(testPass) // generate a single key keyName := "alice" privKeyFileStore, err := trustmanager.NewKeyFileStore(privKeyStorageDir, cannedPasswordRetriever) @@ -87,7 +88,7 @@ func TestGenerateKeySuccess(t *testing.T) { // assert encrypted header assert.Check(t, is.Equal("ENCRYPTED PRIVATE KEY", privKeyPEM.Type)) // check that the passphrase matches - _, err = tufutils.ParsePKCS8ToTufKey(privKeyPEM.Bytes, []byte(passwd)) + _, err = tufutils.ParsePKCS8ToTufKey(privKeyPEM.Bytes, []byte(testPass)) assert.NilError(t, err) // check that the public key exists at the correct path if we use the helper: diff --git a/cli/command/trust/key_load_test.go b/cli/command/trust/key_load_test.go index fae5b858cf95..d32883fc6231 100644 --- a/cli/command/trust/key_load_test.go +++ b/cli/command/trust/key_load_test.go @@ -63,6 +63,7 @@ func TestTrustKeyLoadErrors(t *testing.T) { cmd := newKeyLoadCommand(cli) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) assert.Check(t, is.Contains(cli.OutBuffer().String(), tc.expectedOutput)) } diff --git a/cli/command/trust/revoke_test.go b/cli/command/trust/revoke_test.go index 26e6ef4d014c..7521a7bcf266 100644 --- a/cli/command/trust/revoke_test.go +++ b/cli/command/trust/revoke_test.go @@ -52,6 +52,7 @@ func TestTrustRevokeCommandErrors(t *testing.T) { test.NewFakeCli(&fakeClient{})) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } @@ -139,6 +140,7 @@ func TestTrustRevokeCommand(t *testing.T) { cmd := newRevokeCommand(cli) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) if tc.expectedErr != "" { assert.ErrorContains(t, cmd.Execute(), tc.expectedErr) } else { @@ -164,6 +166,8 @@ func TestRevokeTrustPromptTermination(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cmd := newRevokeCommand(cli) cmd.SetArgs([]string{"example/trust-demo"}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) test.TerminatePrompt(ctx, t, cmd, cli) golden.Assert(t, cli.OutBuffer().String(), "trust-revoke-prompt-termination.golden") } diff --git a/cli/command/trust/sign_test.go b/cli/command/trust/sign_test.go index 632403ebf5d3..8249ad893f41 100644 --- a/cli/command/trust/sign_test.go +++ b/cli/command/trust/sign_test.go @@ -67,6 +67,7 @@ func TestTrustSignCommandErrors(t *testing.T) { test.NewFakeCli(&fakeClient{})) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } @@ -77,6 +78,7 @@ func TestTrustSignCommandOfflineErrors(t *testing.T) { cmd := newSignCommand(cli) cmd.SetArgs([]string{"reg-name.io/image:tag"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), "client is offline") } @@ -260,6 +262,7 @@ func TestSignCommandChangeListIsCleanedOnError(t *testing.T) { cmd := newSignCommand(cli) cmd.SetArgs([]string{"ubuntu:latest"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) err := cmd.Execute() assert.Assert(t, err != nil) @@ -277,5 +280,6 @@ func TestSignCommandLocalFlag(t *testing.T) { cmd := newSignCommand(cli) cmd.SetArgs([]string{"--local", "reg-name.io/image:red"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), "error contacting notary server: dial tcp: lookup reg-name.io") } diff --git a/cli/command/trust/signer_add_test.go b/cli/command/trust/signer_add_test.go index 49e1d11c7c2c..492c3df86e5a 100644 --- a/cli/command/trust/signer_add_test.go +++ b/cli/command/trust/signer_add_test.go @@ -60,6 +60,7 @@ func TestTrustSignerAddErrors(t *testing.T) { cmd := newSignerAddCommand(cli) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } @@ -78,6 +79,7 @@ func TestSignerAddCommandNoTargetsKey(t *testing.T) { cmd.SetArgs([]string{"--key", tmpfile.Name(), "alice", "alpine", "linuxkit/alpine"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.Error(t, cmd.Execute(), fmt.Sprintf("could not parse public key from file: %s: no valid public key found", tmpfile.Name())) } @@ -90,6 +92,7 @@ func TestSignerAddCommandBadKeyPath(t *testing.T) { cmd.SetArgs([]string{"--key", "/path/to/key.pem", "alice", "alpine"}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) expectedError := "unable to read public key from file: open /path/to/key.pem: no such file or directory" if runtime.GOOS == "windows" { expectedError = "unable to read public key from file: open /path/to/key.pem: The system cannot find the path specified." @@ -111,6 +114,7 @@ func TestSignerAddCommandInvalidRepoName(t *testing.T) { cmd.SetArgs([]string{"--key", pubKeyFilepath, "alice", imageName}) cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) assert.Error(t, cmd.Execute(), "failed to add signer to: 870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd") expectedErr := fmt.Sprintf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings\n\n", imageName) diff --git a/cli/command/trust/signer_remove_test.go b/cli/command/trust/signer_remove_test.go index 9b54bcaf2f1a..8e2cc146d909 100644 --- a/cli/command/trust/signer_remove_test.go +++ b/cli/command/trust/signer_remove_test.go @@ -30,41 +30,54 @@ func TestTrustSignerRemoveErrors(t *testing.T) { }, } for _, tc := range testCases { - cmd := newSignerRemoveCommand( - test.NewFakeCli(&fakeClient{})) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cmd := newSignerRemoveCommand( + test.NewFakeCli(&fakeClient{})) + cmd.SetArgs(tc.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } testCasesWithOutput := []struct { - name string - args []string - expectedError string + name string + args []string + expectedError string + expectedErrOut string }{ { - name: "not-an-image", - args: []string{"user", "notanimage"}, - expectedError: "error retrieving signers for notanimage", + name: "not-an-image", + args: []string{"user", "notanimage"}, + expectedError: "error removing signer from: notanimage", + expectedErrOut: "error retrieving signers for notanimage", }, { - name: "sha-reference", - args: []string{"user", "870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"}, - expectedError: "invalid repository name", + name: "sha-reference", + args: []string{"user", "870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"}, + expectedError: "error removing signer from: 870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd", + expectedErrOut: "invalid repository name", }, { - name: "invalid-img-reference", - args: []string{"user", "ALPINE"}, - expectedError: "invalid reference format", + name: "invalid-img-reference", + args: []string{"user", "ALPINE"}, + expectedError: "error removing signer from: ALPINE", + expectedErrOut: "invalid reference format", }, } for _, tc := range testCasesWithOutput { - cli := test.NewFakeCli(&fakeClient{}) - cli.SetNotaryClient(notaryfake.GetOfflineNotaryRepository) - cmd := newSignerRemoveCommand(cli) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - cmd.Execute() - assert.Check(t, is.Contains(cli.ErrBuffer().String(), tc.expectedError)) + tc := tc + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{}) + cli.SetNotaryClient(notaryfake.GetOfflineNotaryRepository) + cmd := newSignerRemoveCommand(cli) + cmd.SetArgs(tc.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + err := cmd.Execute() + assert.Check(t, is.Error(err, tc.expectedError)) + assert.Check(t, is.Contains(cli.ErrBuffer().String(), tc.expectedErrOut)) + }) } } diff --git a/cli/command/utils.go b/cli/command/utils.go index 48d2c4250ace..2b4a885ed9ff 100644 --- a/cli/command/utils.go +++ b/cli/command/utils.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package command @@ -19,6 +19,7 @@ import ( "github.com/docker/docker/api/types/versions" "github.com/docker/docker/errdefs" "github.com/moby/sys/sequential" + "github.com/moby/term" "github.com/pkg/errors" "github.com/spf13/pflag" ) @@ -76,6 +77,48 @@ func PrettyPrint(i any) string { var ErrPromptTerminated = errdefs.Cancelled(errors.New("prompt terminated")) +// DisableInputEcho disables input echo on the provided streams.In. +// This is useful when the user provides sensitive information like passwords. +// The function returns a restore function that should be called to restore the +// terminal state. +func DisableInputEcho(ins *streams.In) (restore func() error, err error) { + oldState, err := term.SaveState(ins.FD()) + if err != nil { + return nil, err + } + restore = func() error { + return term.RestoreTerminal(ins.FD(), oldState) + } + return restore, term.DisableEcho(ins.FD(), oldState) +} + +// PromptForInput requests input from the user. +// +// If the user terminates the CLI with SIGINT or SIGTERM while the prompt is +// active, the prompt will return an empty string ("") with an ErrPromptTerminated error. +// When the prompt returns an error, the caller should propagate the error up +// the stack and close the io.Reader used for the prompt which will prevent the +// background goroutine from blocking indefinitely. +func PromptForInput(ctx context.Context, in io.Reader, out io.Writer, message string) (string, error) { + _, _ = fmt.Fprint(out, message) + + result := make(chan string) + go func() { + scanner := bufio.NewScanner(in) + if scanner.Scan() { + result <- strings.TrimSpace(scanner.Text()) + } + }() + + select { + case <-ctx.Done(): + _, _ = fmt.Fprintln(out, "") + return "", ErrPromptTerminated + case r := <-result: + return r, nil + } +} + // PromptForConfirmation requests and checks confirmation from the user. // This will display the provided message followed by ' [y/N] '. If the user // input 'y' or 'Y' it returns true otherwise false. If no message is provided, @@ -179,7 +222,7 @@ func ValidateOutputPath(path string) error { } if err := ValidateOutputPathFileMode(fileInfo.Mode()); err != nil { - return errors.Wrapf(err, fmt.Sprintf("invalid output path: %q must be a directory or a regular file", path)) + return errors.Wrapf(err, "invalid output path: %q must be a directory or a regular file", path) } } return nil diff --git a/cli/command/utils_test.go b/cli/command/utils_test.go index 1566067f3b38..2f2140757e34 100644 --- a/cli/command/utils_test.go +++ b/cli/command/utils_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/test" "github.com/pkg/errors" "gotest.tools/v3/assert" @@ -80,6 +81,66 @@ func TestValidateOutputPath(t *testing.T) { } } +func TestPromptForInput(t *testing.T) { + t.Run("case=cancelling the context", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + reader, _ := io.Pipe() + + buf := new(bytes.Buffer) + bufioWriter := bufio.NewWriter(buf) + + wroteHook := make(chan struct{}, 1) + promptOut := test.NewWriterWithHook(bufioWriter, func(p []byte) { + wroteHook <- struct{}{} + }) + + promptErr := make(chan error, 1) + go func() { + _, err := command.PromptForInput(ctx, streams.NewIn(reader), streams.NewOut(promptOut), "Enter something") + promptErr <- err + }() + + select { + case <-time.After(1 * time.Second): + t.Fatal("timeout waiting for prompt to write to buffer") + case <-wroteHook: + cancel() + } + + select { + case <-time.After(1 * time.Second): + t.Fatal("timeout waiting for prompt to be canceled") + case err := <-promptErr: + assert.ErrorIs(t, err, command.ErrPromptTerminated) + } + }) + + t.Run("case=user input should be properly trimmed", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + t.Cleanup(cancel) + + reader, writer := io.Pipe() + + buf := new(bytes.Buffer) + bufioWriter := bufio.NewWriter(buf) + + wroteHook := make(chan struct{}, 1) + promptOut := test.NewWriterWithHook(bufioWriter, func(p []byte) { + wroteHook <- struct{}{} + }) + + go func() { + <-wroteHook + writer.Write([]byte(" foo \n")) + }() + + answer, err := command.PromptForInput(ctx, streams.NewIn(reader), streams.NewOut(promptOut), "Enter something") + assert.NilError(t, err) + assert.Equal(t, answer, "foo") + }) +} + func TestPromptForConfirmation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) diff --git a/cli/command/volume/inspect.go b/cli/command/volume/inspect.go index bebc24213d02..015a8040d8e8 100644 --- a/cli/command/volume/inspect.go +++ b/cli/command/volume/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package volume diff --git a/cli/command/volume/prune_test.go b/cli/command/volume/prune_test.go index 528e5aee4be3..5cb1aca34a2a 100644 --- a/cli/command/volume/prune_test.go +++ b/cli/command/volume/prune_test.go @@ -166,15 +166,20 @@ func TestVolumePrunePromptNo(t *testing.T) { skip.If(t, runtime.GOOS == "windows", "TODO: fix test on windows") for _, input := range []string{"n", "N", "no", "anything", "really"} { - cli := test.NewFakeCli(&fakeClient{ - volumePruneFunc: simplePruneFunc, - }) + input := input + t.Run(input, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + volumePruneFunc: simplePruneFunc, + }) - cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(input)))) - cmd := NewPruneCommand(cli) - cmd.SetArgs([]string{}) - assert.ErrorContains(t, cmd.Execute(), "volume prune has been cancelled") - golden.Assert(t, cli.OutBuffer().String(), "volume-prune-no.golden") + cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(input)))) + cmd := NewPruneCommand(cli) + cmd.SetArgs([]string{}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), "volume prune has been cancelled") + golden.Assert(t, cli.OutBuffer().String(), "volume-prune-no.golden") + }) } } @@ -199,6 +204,8 @@ func TestVolumePrunePromptTerminate(t *testing.T) { cmd := NewPruneCommand(cli) cmd.SetArgs([]string{}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) test.TerminatePrompt(ctx, t, cmd, cli) golden.Assert(t, cli.OutBuffer().String(), "volume-prune-terminate.golden") } diff --git a/cli/command/volume/update.go b/cli/command/volume/update.go index c04c2ff8f507..42ce9ac586be 100644 --- a/cli/command/volume/update.go +++ b/cli/command/volume/update.go @@ -18,7 +18,7 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "update [OPTIONS] [VOLUME]", Short: "Update a volume (cluster volumes only)", - Args: cli.RequiresMaxArgs(1), + Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return runUpdate(cmd.Context(), dockerCli, args[0], availability, cmd.Flags()) }, diff --git a/cli/command/volume/update_test.go b/cli/command/volume/update_test.go new file mode 100644 index 000000000000..13051a0056fa --- /dev/null +++ b/cli/command/volume/update_test.go @@ -0,0 +1,22 @@ +package volume + +import ( + "io" + "testing" + + "github.com/docker/cli/internal/test" + "gotest.tools/v3/assert" +) + +func TestUpdateCmd(t *testing.T) { + cmd := newUpdateCommand( + test.NewFakeCli(&fakeClient{}), + ) + cmd.SetArgs([]string{}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + + err := cmd.Execute() + + assert.ErrorContains(t, err, "requires exactly 1 argument") +} diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go index 62cadf57e7e9..44b6de1415c6 100644 --- a/cli/compose/convert/service.go +++ b/cli/compose/convert/service.go @@ -148,6 +148,7 @@ func Service( CapabilityAdd: capAdd, CapabilityDrop: capDrop, Ulimits: convertUlimits(service.Ulimits), + OomScoreAdj: service.OomScoreAdj, }, LogDriver: logDriver, Resources: resources, diff --git a/cli/compose/interpolation/interpolation.go b/cli/compose/interpolation/interpolation.go index 42aefc662044..ee11656f60ac 100644 --- a/cli/compose/interpolation/interpolation.go +++ b/cli/compose/interpolation/interpolation.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package interpolation diff --git a/cli/compose/interpolation/interpolation_test.go b/cli/compose/interpolation/interpolation_test.go index 21fd359c42e2..cc7ffacabbe6 100644 --- a/cli/compose/interpolation/interpolation_test.go +++ b/cli/compose/interpolation/interpolation_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package interpolation diff --git a/cli/compose/loader/full-struct_test.go b/cli/compose/loader/full-struct_test.go index e36204154099..2aa512d09726 100644 --- a/cli/compose/loader/full-struct_test.go +++ b/cli/compose/loader/full-struct_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package loader diff --git a/cli/compose/loader/interpolate.go b/cli/compose/loader/interpolate.go index 445cdeb54786..82c36d7dcaf4 100644 --- a/cli/compose/loader/interpolate.go +++ b/cli/compose/loader/interpolate.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package loader @@ -29,6 +29,7 @@ var interpolateTypeCastMapping = map[interp.Path]interp.Cast{ servicePath("ulimits", interp.PathMatchAll, "hard"): toInt, servicePath("ulimits", interp.PathMatchAll, "soft"): toInt, servicePath("privileged"): toBoolean, + servicePath("oom_score_adj"): toInt, servicePath("read_only"): toBoolean, servicePath("stdin_open"): toBoolean, servicePath("tty"): toBoolean, diff --git a/cli/compose/loader/loader.go b/cli/compose/loader/loader.go index ecf86af9b364..7bc420b2b1e6 100644 --- a/cli/compose/loader/loader.go +++ b/cli/compose/loader/loader.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package loader diff --git a/cli/compose/loader/loader_test.go b/cli/compose/loader/loader_test.go index 47a30ebd0ea3..cc57feca8280 100644 --- a/cli/compose/loader/loader_test.go +++ b/cli/compose/loader/loader_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package loader diff --git a/cli/compose/loader/merge.go b/cli/compose/loader/merge.go index 94c4eebcea5e..34455d597cc7 100644 --- a/cli/compose/loader/merge.go +++ b/cli/compose/loader/merge.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package loader diff --git a/cli/compose/loader/merge_test.go b/cli/compose/loader/merge_test.go index 4d145cc7dfc7..9d5a1dd6cda6 100644 --- a/cli/compose/loader/merge_test.go +++ b/cli/compose/loader/merge_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package loader diff --git a/cli/compose/schema/data/config_schema_v3.13.json b/cli/compose/schema/data/config_schema_v3.13.json index 0a85b3e6c508..8daa8892d625 100644 --- a/cli/compose/schema/data/config_schema_v3.13.json +++ b/cli/compose/schema/data/config_schema_v3.13.json @@ -287,6 +287,7 @@ } } }, + "oom_score_adj": {"type": "integer"}, "user": {"type": "string"}, "userns_mode": {"type": "string"}, "volumes": { diff --git a/cli/compose/schema/schema.go b/cli/compose/schema/schema.go index b4861556a38b..2ef1245bcefe 100644 --- a/cli/compose/schema/schema.go +++ b/cli/compose/schema/schema.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package schema diff --git a/cli/compose/schema/schema_test.go b/cli/compose/schema/schema_test.go index 2f9211130508..cd01e6466c83 100644 --- a/cli/compose/schema/schema_test.go +++ b/cli/compose/schema/schema_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package schema diff --git a/cli/compose/template/template.go b/cli/compose/template/template.go index d97e162b0e1c..1507c0ee6e7f 100644 --- a/cli/compose/template/template.go +++ b/cli/compose/template/template.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package template diff --git a/cli/compose/template/template_test.go b/cli/compose/template/template_test.go index 0d0a92e70953..64abadaf2bf7 100644 --- a/cli/compose/template/template_test.go +++ b/cli/compose/template/template_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package template diff --git a/cli/compose/types/types.go b/cli/compose/types/types.go index 16417f328aaa..55b80365feca 100644 --- a/cli/compose/types/types.go +++ b/cli/compose/types/types.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package types @@ -207,6 +207,7 @@ type ServiceConfig struct { Tty bool `mapstructure:"tty" yaml:"tty,omitempty" json:"tty,omitempty"` Ulimits map[string]*UlimitsConfig `yaml:",omitempty" json:"ulimits,omitempty"` User string `yaml:",omitempty" json:"user,omitempty"` + OomScoreAdj int64 `yaml:",omitempty" json:"oom_score_adj,omitempty"` UserNSMode string `mapstructure:"userns_mode" yaml:"userns_mode,omitempty" json:"userns_mode,omitempty"` Volumes []ServiceVolumeConfig `yaml:",omitempty" json:"volumes,omitempty"` WorkingDir string `mapstructure:"working_dir" yaml:"working_dir,omitempty" json:"working_dir,omitempty"` diff --git a/cli/config/configfile/file.go b/cli/config/configfile/file.go index ba9bc9d1d0db..ae9dcb3370c7 100644 --- a/cli/config/configfile/file.go +++ b/cli/config/configfile/file.go @@ -303,6 +303,7 @@ func (configFile *ConfigFile) GetAllCredentials() (map[string]types.AuthConfig, for registryHostname := range configFile.CredentialHelpers { newAuth, err := configFile.GetAuthConfig(registryHostname) if err != nil { + // TODO(thaJeztah): use context-logger, so that this output can be suppressed (in tests). logrus.WithError(err).Warnf("Failed to get credentials for registry: %s", registryHostname) continue } diff --git a/cli/config/credentials/file_store.go b/cli/config/credentials/file_store.go index 99631bbf4cf7..95406281501c 100644 --- a/cli/config/credentials/file_store.go +++ b/cli/config/credentials/file_store.go @@ -1,6 +1,7 @@ package credentials import ( + "net" "net/url" "strings" @@ -24,8 +25,13 @@ func NewFileStore(file store) Store { return &fileStore{file: file} } -// Erase removes the given credentials from the file store. +// Erase removes the given credentials from the file store.This function is +// idempotent and does not update the file if credentials did not change. func (c *fileStore) Erase(serverAddress string) error { + if _, exists := c.file.GetAuthConfigs()[serverAddress]; !exists { + // nothing to do; no credentials found for the given serverAddress + return nil + } delete(c.file.GetAuthConfigs(), serverAddress) return c.file.Save() } @@ -51,9 +57,14 @@ func (c *fileStore) GetAll() (map[string]types.AuthConfig, error) { return c.file.GetAuthConfigs(), nil } -// Store saves the given credentials in the file store. +// Store saves the given credentials in the file store. This function is +// idempotent and does not update the file if credentials did not change. func (c *fileStore) Store(authConfig types.AuthConfig) error { authConfigs := c.file.GetAuthConfigs() + if oldAuthConfig, ok := authConfigs[authConfig.ServerAddress]; ok && oldAuthConfig == authConfig { + // Credentials didn't change, so skip updating the configuration file. + return nil + } authConfigs[authConfig.ServerAddress] = authConfig return c.file.Save() } @@ -74,7 +85,10 @@ func ConvertToHostname(maybeURL string) string { if strings.Contains(stripped, "://") { u, err := url.Parse(stripped) if err == nil && u.Hostname() != "" { - return u.Hostname() + if u.Port() == "" { + return u.Hostname() + } + return net.JoinHostPort(u.Hostname(), u.Port()) } } hostName, _, _ := strings.Cut(stripped, "/") diff --git a/cli/config/credentials/file_store_test.go b/cli/config/credentials/file_store_test.go index 74d63afcd6fb..466fa054a03e 100644 --- a/cli/config/credentials/file_store_test.go +++ b/cli/config/credentials/file_store_test.go @@ -10,9 +10,15 @@ import ( type fakeStore struct { configs map[string]types.AuthConfig + saveFn func(*fakeStore) error } func (f *fakeStore) Save() error { + if f.saveFn != nil { + // Pass a reference to the fakeStore itself in case saveFn + // wants to access it. + return f.saveFn(f) + } return nil } @@ -21,15 +27,82 @@ func (f *fakeStore) GetAuthConfigs() map[string]types.AuthConfig { } func (f *fakeStore) GetFilename() string { - return "/tmp/docker-fakestore" + return "no-config.json" } -func newStore(auths map[string]types.AuthConfig) store { - return &fakeStore{configs: auths} +// TestFileStoreIdempotent verifies that the config-file isn't updated +// if nothing changed. +func TestFileStoreIdempotent(t *testing.T) { + var saveCount, expectedSaveCount int + + s := NewFileStore(&fakeStore{ + configs: map[string]types.AuthConfig{}, + saveFn: func(*fakeStore) error { + saveCount++ + return nil + }, + }) + authOne := types.AuthConfig{ + Auth: "super_secret_token", + Email: "foo@example.com", + ServerAddress: "https://example.com", + } + authTwo := types.AuthConfig{ + Auth: "also_super_secret_token", + Email: "bar@example.com", + ServerAddress: "https://other.example.com", + } + + expectedSaveCount = 1 + t.Run("store new credentials", func(t *testing.T) { + assert.NilError(t, s.Store(authOne)) + retrievedAuth, err := s.Get(authOne.ServerAddress) + assert.NilError(t, err) + assert.Check(t, is.Equal(retrievedAuth, authOne)) + assert.Check(t, is.Equal(saveCount, expectedSaveCount)) + }) + t.Run("store same credentials is a no-op", func(t *testing.T) { + assert.NilError(t, s.Store(authOne)) + retrievedAuth, err := s.Get(authOne.ServerAddress) + assert.NilError(t, err) + assert.Check(t, is.Equal(retrievedAuth, authOne)) + assert.Check(t, is.Equal(saveCount, expectedSaveCount), "should not have saved if nothing changed") + }) + t.Run("store other credentials", func(t *testing.T) { + expectedSaveCount++ + assert.NilError(t, s.Store(authTwo)) + retrievedAuth, err := s.Get(authTwo.ServerAddress) + assert.NilError(t, err) + assert.Check(t, is.Equal(retrievedAuth, authTwo)) + assert.Check(t, is.Equal(saveCount, expectedSaveCount)) + }) + t.Run("erase credentials", func(t *testing.T) { + expectedSaveCount++ + assert.NilError(t, s.Erase(authOne.ServerAddress)) + retrievedAuth, err := s.Get(authOne.ServerAddress) + assert.NilError(t, err) + assert.Check(t, is.Equal(retrievedAuth, types.AuthConfig{})) + assert.Check(t, is.Equal(saveCount, expectedSaveCount)) + }) + t.Run("erase non-existing credentials is a no-op", func(t *testing.T) { + assert.NilError(t, s.Erase(authOne.ServerAddress)) + retrievedAuth, err := s.Get(authOne.ServerAddress) + assert.NilError(t, err) + assert.Check(t, is.Equal(retrievedAuth, types.AuthConfig{})) + assert.Check(t, is.Equal(saveCount, expectedSaveCount), "should not have saved if nothing changed") + }) + t.Run("erase other credentials", func(t *testing.T) { + expectedSaveCount++ + assert.NilError(t, s.Erase(authTwo.ServerAddress)) + retrievedAuth, err := s.Get(authTwo.ServerAddress) + assert.NilError(t, err) + assert.Check(t, is.Equal(retrievedAuth, types.AuthConfig{})) + assert.Check(t, is.Equal(saveCount, expectedSaveCount)) + }) } func TestFileStoreAddCredentials(t *testing.T) { - f := newStore(make(map[string]types.AuthConfig)) + f := &fakeStore{configs: map[string]types.AuthConfig{}} s := NewFileStore(f) auth := types.AuthConfig{ @@ -47,13 +120,13 @@ func TestFileStoreAddCredentials(t *testing.T) { } func TestFileStoreGet(t *testing.T) { - f := newStore(map[string]types.AuthConfig{ + f := &fakeStore{configs: map[string]types.AuthConfig{ "https://example.com": { Auth: "super_secret_token", Email: "foo@example.com", ServerAddress: "https://example.com", }, - }) + }} s := NewFileStore(f) a, err := s.Get("https://example.com") @@ -71,7 +144,7 @@ func TestFileStoreGet(t *testing.T) { func TestFileStoreGetAll(t *testing.T) { s1 := "https://example.com" s2 := "https://example2.example.com" - f := newStore(map[string]types.AuthConfig{ + f := &fakeStore{configs: map[string]types.AuthConfig{ s1: { Auth: "super_secret_token", Email: "foo@example.com", @@ -82,7 +155,7 @@ func TestFileStoreGetAll(t *testing.T) { Email: "foo@example2.com", ServerAddress: "https://example2.example.com", }, - }) + }} s := NewFileStore(f) as, err := s.GetAll() @@ -107,13 +180,13 @@ func TestFileStoreGetAll(t *testing.T) { } func TestFileStoreErase(t *testing.T) { - f := newStore(map[string]types.AuthConfig{ + f := &fakeStore{configs: map[string]types.AuthConfig{ "https://example.com": { Auth: "super_secret_token", Email: "foo@example.com", ServerAddress: "https://example.com", }, - }) + }} s := NewFileStore(f) err := s.Erase("https://example.com") @@ -137,6 +210,19 @@ func TestFileStoreErase(t *testing.T) { func TestConvertToHostname(t *testing.T) { tests := []struct{ input, expected string }{ + { + input: "127.0.0.1", + expected: "127.0.0.1", + }, + { + input: "::1", + expected: "::1", + }, + { + // FIXME(thaJeztah): this should be normalized to "::1" if there's no port (or vice-versa, as long as we're consistent) + input: "[::1]", + expected: "[::1]", + }, { input: "example.com", expected: "example.com", @@ -167,6 +253,48 @@ func TestConvertToHostname(t *testing.T) { input: "ftp://example.com", expected: "example.com", }, + // should support non-standard port in registry url + { + input: "127.0.0.1:6556", + expected: "127.0.0.1:6556", + }, + { + // FIXME(thaJeztah): this should be normalized to "[::1]:6556" + input: "::1:6556", + expected: "::1:6556", + }, + { + input: "[::1]:6556", + expected: "[::1]:6556", + }, + { + input: "example.com:6555", + expected: "example.com:6555", + }, + { + input: "https://127.0.0.1:6555/v2/", + expected: "127.0.0.1:6555", + }, + { + input: "https://::1:6555/v2/", + expected: "[::1]:6555", + }, + { + input: "https://[::1]:6555/v2/", + expected: "[::1]:6555", + }, + { + input: "http://example.com:6555", + expected: "example.com:6555", + }, + { + input: "https://example.com:6555", + expected: "example.com:6555", + }, + { + input: "https://example.com:6555/v2/", + expected: "example.com:6555", + }, } for _, tc := range tests { tc := tc diff --git a/cli/config/credentials/native_store_test.go b/cli/config/credentials/native_store_test.go index 5abcca3587e0..f55d269ed59f 100644 --- a/cli/config/credentials/native_store_test.go +++ b/cli/config/credentials/native_store_test.go @@ -91,7 +91,7 @@ func mockCommandFn(args ...string) client.Program { } func TestNativeStoreAddCredentials(t *testing.T) { - f := newStore(make(map[string]types.AuthConfig)) + f := &fakeStore{configs: map[string]types.AuthConfig{}} s := &nativeStore{ programFunc: mockCommandFn, fileStore: NewFileStore(f), @@ -116,7 +116,7 @@ func TestNativeStoreAddCredentials(t *testing.T) { } func TestNativeStoreAddInvalidCredentials(t *testing.T) { - f := newStore(make(map[string]types.AuthConfig)) + f := &fakeStore{configs: map[string]types.AuthConfig{}} s := &nativeStore{ programFunc: mockCommandFn, fileStore: NewFileStore(f), @@ -132,11 +132,11 @@ func TestNativeStoreAddInvalidCredentials(t *testing.T) { } func TestNativeStoreGet(t *testing.T) { - f := newStore(map[string]types.AuthConfig{ + f := &fakeStore{configs: map[string]types.AuthConfig{ validServerAddress: { Email: "foo@example.com", }, - }) + }} s := &nativeStore{ programFunc: mockCommandFn, fileStore: NewFileStore(f), @@ -154,11 +154,11 @@ func TestNativeStoreGet(t *testing.T) { } func TestNativeStoreGetIdentityToken(t *testing.T) { - f := newStore(map[string]types.AuthConfig{ + f := &fakeStore{configs: map[string]types.AuthConfig{ validServerAddress2: { Email: "foo@example2.com", }, - }) + }} s := &nativeStore{ programFunc: mockCommandFn, @@ -176,11 +176,11 @@ func TestNativeStoreGetIdentityToken(t *testing.T) { } func TestNativeStoreGetAll(t *testing.T) { - f := newStore(map[string]types.AuthConfig{ + f := &fakeStore{configs: map[string]types.AuthConfig{ validServerAddress: { Email: "foo@example.com", }, - }) + }} s := &nativeStore{ programFunc: mockCommandFn, @@ -217,11 +217,11 @@ func TestNativeStoreGetAll(t *testing.T) { } func TestNativeStoreGetMissingCredentials(t *testing.T) { - f := newStore(map[string]types.AuthConfig{ + f := &fakeStore{configs: map[string]types.AuthConfig{ validServerAddress: { Email: "foo@example.com", }, - }) + }} s := &nativeStore{ programFunc: mockCommandFn, @@ -232,11 +232,11 @@ func TestNativeStoreGetMissingCredentials(t *testing.T) { } func TestNativeStoreGetInvalidAddress(t *testing.T) { - f := newStore(map[string]types.AuthConfig{ + f := &fakeStore{configs: map[string]types.AuthConfig{ validServerAddress: { Email: "foo@example.com", }, - }) + }} s := &nativeStore{ programFunc: mockCommandFn, @@ -247,11 +247,11 @@ func TestNativeStoreGetInvalidAddress(t *testing.T) { } func TestNativeStoreErase(t *testing.T) { - f := newStore(map[string]types.AuthConfig{ + f := &fakeStore{configs: map[string]types.AuthConfig{ validServerAddress: { Email: "foo@example.com", }, - }) + }} s := &nativeStore{ programFunc: mockCommandFn, @@ -263,11 +263,11 @@ func TestNativeStoreErase(t *testing.T) { } func TestNativeStoreEraseInvalidAddress(t *testing.T) { - f := newStore(map[string]types.AuthConfig{ + f := &fakeStore{configs: map[string]types.AuthConfig{ validServerAddress: { Email: "foo@example.com", }, - }) + }} s := &nativeStore{ programFunc: mockCommandFn, diff --git a/cli/connhelper/commandconn/commandconn_unix_test.go b/cli/connhelper/commandconn/commandconn_unix_test.go index baa70cce82ab..6c1219f4386e 100644 --- a/cli/connhelper/commandconn/commandconn_unix_test.go +++ b/cli/connhelper/commandconn/commandconn_unix_test.go @@ -48,7 +48,7 @@ func TestCloseRunningCommand(t *testing.T) { defer close(done) go func() { - c, err := New(ctx, "sh", "-c", "while true; sleep 1; done") + c, err := New(ctx, "sh", "-c", "while true; do sleep 1; done") assert.NilError(t, err) cmdConn := c.(*commandConn) assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid)) @@ -145,7 +145,7 @@ func (mockStdoutEOF) Close() error { func TestCloseWhileWriting(t *testing.T) { ctx := context.TODO() - c, err := New(ctx, "sh", "-c", "while true; sleep 1; done") + c, err := New(ctx, "sh", "-c", "while true; do sleep 1; done") assert.NilError(t, err) cmdConn := c.(*commandConn) assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid)) @@ -173,7 +173,7 @@ func TestCloseWhileWriting(t *testing.T) { func TestCloseWhileReading(t *testing.T) { ctx := context.TODO() - c, err := New(ctx, "sh", "-c", "while true; sleep 1; done") + c, err := New(ctx, "sh", "-c", "while true; do sleep 1; done") assert.NilError(t, err) cmdConn := c.(*commandConn) assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid)) diff --git a/cli/connhelper/connhelper.go b/cli/connhelper/connhelper.go index 1797abaed4c7..152d3e2953a8 100644 --- a/cli/connhelper/connhelper.go +++ b/cli/connhelper/connhelper.go @@ -45,13 +45,14 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*ConnectionHelper if err != nil { return nil, errors.Wrap(err, "ssh host connection is not valid") } + sshFlags = addSSHTimeout(sshFlags) + sshFlags = disablePseudoTerminalAllocation(sshFlags) return &ConnectionHelper{ Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { args := []string{"docker"} if sp.Path != "" { args = append(args, "--host", "unix://"+sp.Path) } - sshFlags = addSSHTimeout(sshFlags) args = append(args, "system", "dial-stdio") return commandconn.New(ctx, "ssh", append(sshFlags, sp.Args(args...)...)...) }, @@ -79,3 +80,14 @@ func addSSHTimeout(sshFlags []string) []string { } return sshFlags } + +// disablePseudoTerminalAllocation disables pseudo-terminal allocation to +// prevent SSH from executing as a login shell +func disablePseudoTerminalAllocation(sshFlags []string) []string { + for _, flag := range sshFlags { + if flag == "-T" { + return sshFlags + } + } + return append(sshFlags, "-T") +} diff --git a/cli/connhelper/connhelper_test.go b/cli/connhelper/connhelper_test.go index 14384f5c8552..0d9aee0fb2c6 100644 --- a/cli/connhelper/connhelper_test.go +++ b/cli/connhelper/connhelper_test.go @@ -1,6 +1,7 @@ package connhelper import ( + "reflect" "testing" "gotest.tools/v3/assert" @@ -29,3 +30,36 @@ func TestSSHFlags(t *testing.T) { assert.DeepEqual(t, addSSHTimeout(tc.in), tc.out) } } + +func TestDisablePseudoTerminalAllocation(t *testing.T) { + testCases := []struct { + name string + sshFlags []string + expected []string + }{ + { + name: "No -T flag present", + sshFlags: []string{"-v", "-oStrictHostKeyChecking=no"}, + expected: []string{"-v", "-oStrictHostKeyChecking=no", "-T"}, + }, + { + name: "Already contains -T flag", + sshFlags: []string{"-v", "-T", "-oStrictHostKeyChecking=no"}, + expected: []string{"-v", "-T", "-oStrictHostKeyChecking=no"}, + }, + { + name: "Empty sshFlags", + sshFlags: []string{}, + expected: []string{"-T"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := disablePseudoTerminalAllocation(tc.sshFlags) + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("expected %v, got %v", tc.expected, result) + } + }) + } +} diff --git a/cli/context/store/metadata_test.go b/cli/context/store/metadata_test.go index b37ced034551..3d3a6606adae 100644 --- a/cli/context/store/metadata_test.go +++ b/cli/context/store/metadata_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package store diff --git a/cli/context/store/metadatastore.go b/cli/context/store/metadatastore.go index 0c6986fa7510..6b8975a4ac9c 100644 --- a/cli/context/store/metadatastore.go +++ b/cli/context/store/metadatastore.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package store diff --git a/cli/context/store/store.go b/cli/context/store/store.go index 44e9477fb014..3643e5765935 100644 --- a/cli/context/store/store.go +++ b/cli/context/store/store.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package store @@ -124,6 +124,9 @@ func (s *ContextStore) List() ([]Metadata, error) { // Names return Metadata names for a Lister func Names(s Lister) ([]string, error) { + if s == nil { + return nil, errors.New("nil lister") + } list, err := s.List() if err != nil { return nil, err diff --git a/cli/context/store/store_test.go b/cli/context/store/store_test.go index ad61036dbe52..0f00f4a94114 100644 --- a/cli/context/store/store_test.go +++ b/cli/context/store/store_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package store @@ -260,3 +260,9 @@ func TestCorruptMetadata(t *testing.T) { _, err = s.GetMetadata("source") assert.ErrorContains(t, err, fmt.Sprintf("parsing %s: unexpected end of JSON input", contextFile)) } + +func TestNames(t *testing.T) { + names, err := Names(nil) + assert.Check(t, is.Error(err, "nil lister")) + assert.Check(t, is.Len(names, 0)) +} diff --git a/cli/context/store/storeconfig.go b/cli/context/store/storeconfig.go index aa516a191e29..bfd5e6fcd453 100644 --- a/cli/context/store/storeconfig.go +++ b/cli/context/store/storeconfig.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package store diff --git a/cli/context/store/storeconfig_test.go b/cli/context/store/storeconfig_test.go index 22d40e852103..da3d7e9e75a1 100644 --- a/cli/context/store/storeconfig_test.go +++ b/cli/context/store/storeconfig_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package store diff --git a/cli/error.go b/cli/error.go index 62f62433b82b..a821f9e5a8c8 100644 --- a/cli/error.go +++ b/cli/error.go @@ -8,6 +8,8 @@ import ( // Errors is a list of errors. // Useful in a loop if you don't want to return the error right away and you want to display after the loop, // all the errors that happened during the loop. +// +// Deprecated: use [errors.Join] instead; will be removed in the next release. type Errors []error func (errList Errors) Error() string { diff --git a/cli/internal/oauth/api/api.go b/cli/internal/oauth/api/api.go new file mode 100644 index 000000000000..98c981f0290f --- /dev/null +++ b/cli/internal/oauth/api/api.go @@ -0,0 +1,261 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.22 + +package api + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "runtime" + "strings" + "time" + + "github.com/docker/cli/cli/version" +) + +type OAuthAPI interface { + GetDeviceCode(ctx context.Context, audience string) (State, error) + WaitForDeviceToken(ctx context.Context, state State) (TokenResponse, error) + RevokeToken(ctx context.Context, refreshToken string) error + GetAutoPAT(ctx context.Context, audience string, res TokenResponse) (string, error) +} + +// API represents API interactions with Auth0. +type API struct { + // TenantURL is the base used for each request to Auth0. + TenantURL string + // ClientID is the client ID for the application to auth with the tenant. + ClientID string + // Scopes are the scopes that are requested during the device auth flow. + Scopes []string +} + +// TokenResponse represents the response of the /oauth/token route. +type TokenResponse struct { + AccessToken string `json:"access_token"` + IDToken string `json:"id_token"` + RefreshToken string `json:"refresh_token"` + Scope string `json:"scope"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` + Error *string `json:"error,omitempty"` + ErrorDescription string `json:"error_description,omitempty"` +} + +var ErrTimeout = errors.New("timed out waiting for device token") + +// GetDeviceCode initiates the device-code auth flow with the tenant. +// The state returned contains the device code that the user must use to +// authenticate, as well as the URL to visit, etc. +func (a API) GetDeviceCode(ctx context.Context, audience string) (State, error) { + data := url.Values{ + "client_id": {a.ClientID}, + "audience": {audience}, + "scope": {strings.Join(a.Scopes, " ")}, + } + + deviceCodeURL := a.TenantURL + "/oauth/device/code" + resp, err := postForm(ctx, deviceCodeURL, strings.NewReader(data.Encode())) + if err != nil { + return State{}, err + } + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + return State{}, tryDecodeOAuthError(resp) + } + + var state State + err = json.NewDecoder(resp.Body).Decode(&state) + if err != nil { + return state, fmt.Errorf("failed to get device code: %w", err) + } + + return state, nil +} + +func tryDecodeOAuthError(resp *http.Response) error { + var body map[string]any + if err := json.NewDecoder(resp.Body).Decode(&body); err == nil { + if errorDescription, ok := body["error_description"].(string); ok { + return errors.New(errorDescription) + } + } + return errors.New("unexpected response from tenant: " + resp.Status) +} + +// WaitForDeviceToken polls the tenant to get access/refresh tokens for the user. +// This should be called after GetDeviceCode, and will block until the user has +// authenticated or we have reached the time limit for authenticating (based on +// the response from GetDeviceCode). +func (a API) WaitForDeviceToken(ctx context.Context, state State) (TokenResponse, error) { + // Ticker for polling tenant for login – based on the interval + // specified by the tenant response. + ticker := time.NewTimer(state.IntervalDuration()) + defer ticker.Stop() + // The tenant tells us for as long as we can poll it for credentials + // while the user logs in through their browser. Timeout if we don't get + // credentials within this period. + timeout := time.NewTimer(state.ExpiryDuration()) + defer timeout.Stop() + + for { + resetTimer(ticker, state.IntervalDuration()) + select { + case <-ctx.Done(): + // user canceled login + return TokenResponse{}, ctx.Err() + case <-ticker.C: + // tick, check for user login + res, err := a.getDeviceToken(ctx, state) + if err != nil { + if errors.Is(err, context.Canceled) { + // if the caller canceled the context, continue + // and let the select hit the ctx.Done() branch + continue + } + return TokenResponse{}, err + } + + if res.Error != nil { + if *res.Error == "authorization_pending" { + continue + } + + return res, errors.New(res.ErrorDescription) + } + + return res, nil + case <-timeout.C: + // login timed out + return TokenResponse{}, ErrTimeout + } + } +} + +// resetTimer is a helper function thatstops, drains and resets the timer. +// This is necessary in go versions <1.23, since the timer isn't stopped + +// the timer's channel isn't drained on timer.Reset. +// See: https://go-review.googlesource.com/c/go/+/568341 +// FIXME: remove/simplify this after we update to go1.23 +func resetTimer(t *time.Timer, d time.Duration) { + if !t.Stop() { + select { + case <-t.C: + default: + } + } + t.Reset(d) +} + +// getToken calls the token endpoint of Auth0 and returns the response. +func (a API) getDeviceToken(ctx context.Context, state State) (TokenResponse, error) { + ctx, cancel := context.WithTimeout(ctx, 1*time.Minute) + defer cancel() + + data := url.Values{ + "client_id": {a.ClientID}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + "device_code": {state.DeviceCode}, + } + oauthTokenURL := a.TenantURL + "/oauth/token" + + resp, err := postForm(ctx, oauthTokenURL, strings.NewReader(data.Encode())) + if err != nil { + return TokenResponse{}, fmt.Errorf("failed to get tokens: %w", err) + } + defer func() { + _ = resp.Body.Close() + }() + + // this endpoint returns a 403 with an `authorization_pending` error until the + // user has authenticated, so we don't check the status code here and instead + // decode the response and check for the error. + var res TokenResponse + err = json.NewDecoder(resp.Body).Decode(&res) + if err != nil { + return res, fmt.Errorf("failed to decode response: %w", err) + } + + return res, nil +} + +// RevokeToken revokes a refresh token with the tenant so that it can no longer +// be used to get new tokens. +func (a API) RevokeToken(ctx context.Context, refreshToken string) error { + data := url.Values{ + "client_id": {a.ClientID}, + "token": {refreshToken}, + } + + revokeURL := a.TenantURL + "/oauth/revoke" + resp, err := postForm(ctx, revokeURL, strings.NewReader(data.Encode())) + if err != nil { + return err + } + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + return tryDecodeOAuthError(resp) + } + + return nil +} + +func postForm(ctx context.Context, reqURL string, data io.Reader) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, data) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + cliVersion := strings.ReplaceAll(version.Version, ".", "_") + req.Header.Set("User-Agent", fmt.Sprintf("docker-cli:%s:%s-%s", cliVersion, runtime.GOOS, runtime.GOARCH)) + + return http.DefaultClient.Do(req) +} + +func (a API) GetAutoPAT(ctx context.Context, audience string, res TokenResponse) (string, error) { + patURL := audience + "/v2/access-tokens/desktop-generate" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, patURL, nil) + if err != nil { + return "", err + } + + req.Header.Set("Authorization", "Bearer "+res.AccessToken) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusCreated { + return "", fmt.Errorf("unexpected response from Hub: %s", resp.Status) + } + + var response patGenerateResponse + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return "", err + } + + return response.Data.Token, nil +} + +type patGenerateResponse struct { + Data struct { + Token string `json:"token"` + } +} diff --git a/cli/internal/oauth/api/api_test.go b/cli/internal/oauth/api/api_test.go new file mode 100644 index 000000000000..ec8ea876ff77 --- /dev/null +++ b/cli/internal/oauth/api/api_test.go @@ -0,0 +1,428 @@ +package api + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + "time" + + "gotest.tools/v3/assert" +) + +func TestGetDeviceCode(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + var clientID, audience, scope, path string + expectedState := State{ + DeviceCode: "aDeviceCode", + UserCode: "aUserCode", + VerificationURI: "aVerificationURI", + ExpiresIn: 60, + } + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + clientID = r.FormValue("client_id") + audience = r.FormValue("audience") + scope = r.FormValue("scope") + path = r.URL.Path + + jsonState, err := json.Marshal(expectedState) + assert.NilError(t, err) + + _, _ = w.Write(jsonState) + })) + defer ts.Close() + api := API{ + TenantURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + + state, err := api.GetDeviceCode(context.Background(), "anAudience") + assert.NilError(t, err) + + assert.DeepEqual(t, expectedState, state) + assert.Equal(t, clientID, "aClientID") + assert.Equal(t, audience, "anAudience") + assert.Equal(t, scope, "bork meow") + assert.Equal(t, path, "/oauth/device/code") + }) + + t.Run("error w/ description", func(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + jsonState, err := json.Marshal(TokenResponse{ + ErrorDescription: "invalid audience", + }) + assert.NilError(t, err) + + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write(jsonState) + })) + defer ts.Close() + api := API{ + TenantURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + + _, err := api.GetDeviceCode(context.Background(), "bad_audience") + + assert.ErrorContains(t, err, "invalid audience") + }) + + t.Run("general error", func(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "an error", http.StatusInternalServerError) + })) + defer ts.Close() + api := API{ + TenantURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + + _, err := api.GetDeviceCode(context.Background(), "anAudience") + + assert.ErrorContains(t, err, "unexpected response from tenant: 500 Internal Server Error") + }) + + t.Run("canceled context", func(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + time.Sleep(2 * time.Second) + http.Error(w, "an error", http.StatusInternalServerError) + })) + defer ts.Close() + api := API{ + TenantURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + + ctx, cancel := context.WithCancel(context.Background()) + go func() { + time.Sleep(1 * time.Second) + cancel() + }() + _, err := api.GetDeviceCode(ctx, "anAudience") + + assert.ErrorContains(t, err, "context canceled") + }) +} + +func TestWaitForDeviceToken(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + expectedToken := TokenResponse{ + AccessToken: "a-real-token", + IDToken: "", + RefreshToken: "the-refresh-token", + Scope: "", + ExpiresIn: 3600, + TokenType: "", + } + var respond atomic.Bool + go func() { + time.Sleep(5 * time.Second) + respond.Store(true) + }() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/oauth/token", r.URL.Path) + assert.Equal(t, r.FormValue("client_id"), "aClientID") + assert.Equal(t, r.FormValue("grant_type"), "urn:ietf:params:oauth:grant-type:device_code") + assert.Equal(t, r.FormValue("device_code"), "aDeviceCode") + + if respond.Load() { + jsonState, err := json.Marshal(expectedToken) + assert.NilError(t, err) + w.Write(jsonState) + } else { + pendingError := "authorization_pending" + jsonResponse, err := json.Marshal(TokenResponse{ + Error: &pendingError, + }) + assert.NilError(t, err) + w.Write(jsonResponse) + } + })) + defer ts.Close() + api := API{ + TenantURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + state := State{ + DeviceCode: "aDeviceCode", + UserCode: "aUserCode", + Interval: 1, + ExpiresIn: 30, + } + token, err := api.WaitForDeviceToken(context.Background(), state) + assert.NilError(t, err) + + assert.DeepEqual(t, token, expectedToken) + }) + + t.Run("timeout", func(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/oauth/token", r.URL.Path) + assert.Equal(t, r.FormValue("client_id"), "aClientID") + assert.Equal(t, r.FormValue("grant_type"), "urn:ietf:params:oauth:grant-type:device_code") + assert.Equal(t, r.FormValue("device_code"), "aDeviceCode") + + pendingError := "authorization_pending" + jsonResponse, err := json.Marshal(TokenResponse{ + Error: &pendingError, + }) + assert.NilError(t, err) + w.Write(jsonResponse) + })) + defer ts.Close() + api := API{ + TenantURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + state := State{ + DeviceCode: "aDeviceCode", + UserCode: "aUserCode", + Interval: 5, + ExpiresIn: 1, + } + + _, err := api.WaitForDeviceToken(context.Background(), state) + + assert.ErrorIs(t, err, ErrTimeout) + }) + + t.Run("canceled context", func(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + pendingError := "authorization_pending" + jsonResponse, err := json.Marshal(TokenResponse{ + Error: &pendingError, + }) + assert.NilError(t, err) + w.Write(jsonResponse) + })) + defer ts.Close() + api := API{ + TenantURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + state := State{ + DeviceCode: "aDeviceCode", + UserCode: "aUserCode", + Interval: 1, + ExpiresIn: 5, + } + + ctx, cancel := context.WithCancel(context.Background()) + go func() { + time.Sleep(1 * time.Second) + cancel() + }() + _, err := api.WaitForDeviceToken(ctx, state) + + assert.ErrorContains(t, err, "context canceled") + }) +} + +func TestRevoke(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/oauth/revoke", r.URL.Path) + assert.Equal(t, r.FormValue("client_id"), "aClientID") + assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token") + + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + api := API{ + TenantURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + + err := api.RevokeToken(context.Background(), "v1.a-refresh-token") + assert.NilError(t, err) + }) + + t.Run("unexpected response", func(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/oauth/revoke", r.URL.Path) + assert.Equal(t, r.FormValue("client_id"), "aClientID") + assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token") + + w.WriteHeader(http.StatusNotFound) + })) + defer ts.Close() + api := API{ + TenantURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + + err := api.RevokeToken(context.Background(), "v1.a-refresh-token") + assert.ErrorContains(t, err, "unexpected response from tenant: 404 Not Found") + }) + + t.Run("error w/ description", func(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + jsonState, err := json.Marshal(TokenResponse{ + ErrorDescription: "invalid client id", + }) + assert.NilError(t, err) + + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write(jsonState) + })) + defer ts.Close() + api := API{ + TenantURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + + err := api.RevokeToken(context.Background(), "v1.a-refresh-token") + assert.ErrorContains(t, err, "invalid client id") + }) + + t.Run("canceled context", func(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/oauth/revoke", r.URL.Path) + assert.Equal(t, r.FormValue("client_id"), "aClientID") + assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token") + + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + api := API{ + TenantURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + err := api.RevokeToken(ctx, "v1.a-refresh-token") + + assert.ErrorContains(t, err, "context canceled") + }) +} + +func TestGetAutoPAT(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/v2/access-tokens/desktop-generate", r.URL.Path) + assert.Equal(t, "Bearer bork", r.Header.Get("Authorization")) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + + marshalledResponse, err := json.Marshal(patGenerateResponse{ + Data: struct { + Token string `json:"token"` + }{ + Token: "a-docker-pat", + }, + }) + assert.NilError(t, err) + w.WriteHeader(http.StatusCreated) + w.Write(marshalledResponse) + })) + defer ts.Close() + api := API{ + TenantURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + + pat, err := api.GetAutoPAT(context.Background(), ts.URL, TokenResponse{ + AccessToken: "bork", + }) + assert.NilError(t, err) + + assert.Equal(t, "a-docker-pat", pat) + }) + + t.Run("general error", func(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer ts.Close() + api := API{ + TenantURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + + _, err := api.GetAutoPAT(context.Background(), ts.URL, TokenResponse{ + AccessToken: "bork", + }) + assert.ErrorContains(t, err, "unexpected response from Hub: 500 Internal Server Error") + }) + + t.Run("context canceled", func(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/v2/access-tokens/desktop-generate", r.URL.Path) + assert.Equal(t, "Bearer bork", r.Header.Get("Authorization")) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + + marshalledResponse, err := json.Marshal(patGenerateResponse{ + Data: struct { + Token string `json:"token"` + }{ + Token: "a-docker-pat", + }, + }) + assert.NilError(t, err) + + time.Sleep(500 * time.Millisecond) + w.WriteHeader(http.StatusCreated) + w.Write(marshalledResponse) + })) + defer ts.Close() + api := API{ + TenantURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + pat, err := api.GetAutoPAT(ctx, ts.URL, TokenResponse{ + AccessToken: "bork", + }) + + assert.ErrorContains(t, err, "context canceled") + assert.Equal(t, "", pat) + }) +} diff --git a/cli/internal/oauth/api/state.go b/cli/internal/oauth/api/state.go new file mode 100644 index 000000000000..099fe49eca1e --- /dev/null +++ b/cli/internal/oauth/api/state.go @@ -0,0 +1,26 @@ +package api + +import ( + "time" +) + +// State represents the state of exchange after submitting. +type State struct { + DeviceCode string `json:"device_code"` + UserCode string `json:"user_code"` + VerificationURI string `json:"verification_uri_complete"` + ExpiresIn int `json:"expires_in"` + Interval int `json:"interval"` +} + +// IntervalDuration returns the duration that should be waited between each auth +// polling event. +func (s State) IntervalDuration() time.Duration { + return time.Second * time.Duration(s.Interval) +} + +// ExpiryDuration returns the total duration for which the client should keep +// polling. +func (s State) ExpiryDuration() time.Duration { + return time.Second * time.Duration(s.ExpiresIn) +} diff --git a/cli/internal/oauth/jwt.go b/cli/internal/oauth/jwt.go new file mode 100644 index 000000000000..9397ff179587 --- /dev/null +++ b/cli/internal/oauth/jwt.go @@ -0,0 +1,73 @@ +package oauth + +import ( + "github.com/go-jose/go-jose/v3/jwt" +) + +// Claims represents standard claims along with some custom ones. +type Claims struct { + jwt.Claims + + // Domain is the domain claims for the token. + Domain DomainClaims `json:"https://hub.docker.com"` + + // Scope is the scopes for the claims as a string that is space delimited. + Scope string `json:"scope,omitempty"` +} + +// DomainClaims represents a custom claim data set that doesn't change the spec +// payload. This is primarily introduced by Auth0 and is defined by a fully +// specified URL as it's key. e.g. "https://hub.docker.com" +type DomainClaims struct { + // UUID is the user, machine client, or organization's UUID in our database. + UUID string `json:"uuid"` + + // Email is the user's email address. + Email string `json:"email"` + + // Username is the user's username. + Username string `json:"username"` + + // Source is the source of the JWT. This should look like + // `docker_{type}|{id}`. + Source string `json:"source"` + + // SessionID is the unique ID of the token. + SessionID string `json:"session_id"` + + // ClientID is the client_id that generated the token. This is filled if + // M2M. + ClientID string `json:"client_id,omitempty"` + + // ClientName is the name of the client that generated the token. This is + // filled if M2M. + ClientName string `json:"client_name,omitempty"` +} + +// Source represents a source of a JWT. +type Source struct { + // Type is the type of source. This could be "pat" etc. + Type string `json:"type"` + + // ID is the identifier to the source type. If "pat" then this will be the + // ID of the PAT. + ID string `json:"id"` +} + +// GetClaims returns claims from an access token without verification. +func GetClaims(accessToken string) (claims Claims, err error) { + token, err := parseSigned(accessToken) + if err != nil { + return + } + + err = token.UnsafeClaimsWithoutVerification(&claims) + + return +} + +// parseSigned parses a JWT and returns the signature object or error. This does +// not verify the validity of the JWT. +func parseSigned(token string) (*jwt.JSONWebToken, error) { + return jwt.ParseSigned(token) +} diff --git a/cli/internal/oauth/manager/manager.go b/cli/internal/oauth/manager/manager.go new file mode 100644 index 000000000000..82f541f48b79 --- /dev/null +++ b/cli/internal/oauth/manager/manager.go @@ -0,0 +1,204 @@ +package manager + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/docker/cli/cli/config/credentials" + "github.com/docker/cli/cli/config/types" + "github.com/docker/cli/cli/internal/oauth" + "github.com/docker/cli/cli/internal/oauth/api" + "github.com/docker/docker/registry" + "github.com/morikuni/aec" + "github.com/sirupsen/logrus" + + "github.com/pkg/browser" +) + +// OAuthManager is the manager responsible for handling authentication +// flows with the oauth tenant. +type OAuthManager struct { + store credentials.Store + tenant string + audience string + clientID string + api api.OAuthAPI + openBrowser func(string) error +} + +// OAuthManagerOptions are the options used for New to create a new auth manager. +type OAuthManagerOptions struct { + Store credentials.Store + Audience string + ClientID string + Scopes []string + Tenant string + DeviceName string + OpenBrowser func(string) error +} + +func New(options OAuthManagerOptions) *OAuthManager { + scopes := []string{"openid", "offline_access"} + if len(options.Scopes) > 0 { + scopes = options.Scopes + } + + openBrowser := options.OpenBrowser + if openBrowser == nil { + // Prevent errors from missing binaries (like xdg-open) from + // cluttering the output. We can handle errors ourselves. + browser.Stdout = io.Discard + browser.Stderr = io.Discard + openBrowser = browser.OpenURL + } + + return &OAuthManager{ + clientID: options.ClientID, + audience: options.Audience, + tenant: options.Tenant, + store: options.Store, + api: api.API{ + TenantURL: "https://" + options.Tenant, + ClientID: options.ClientID, + Scopes: scopes, + }, + openBrowser: openBrowser, + } +} + +var ErrDeviceLoginStartFail = errors.New("failed to start device code flow login") + +// LoginDevice launches the device authentication flow with the tenant, +// printing instructions to the provided writer and attempting to open the +// browser for the user to authenticate. +// After the user completes the browser login, LoginDevice uses the retrieved +// tokens to create a Hub PAT which is returned to the caller. +// The retrieved tokens are stored in the credentials store (under a separate +// key), and the refresh token is concatenated with the client ID. +func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.AuthConfig, error) { + state, err := m.api.GetDeviceCode(ctx, m.audience) + if err != nil { + logrus.Debugf("failed to start device code login: %v", err) + return nil, ErrDeviceLoginStartFail + } + + if state.UserCode == "" { + logrus.Debugf("failed to start device code login: missing user code") + return nil, ErrDeviceLoginStartFail + } + + _, _ = fmt.Fprintln(w, aec.Bold.Apply("\nUSING WEB-BASED LOGIN")) + _, _ = fmt.Fprintln(w, "To sign in with credentials on the command line, use 'docker login -u '") + _, _ = fmt.Fprintf(w, "\nYour one-time device confirmation code is: "+aec.Bold.Apply("%s\n"), state.UserCode) + _, _ = fmt.Fprintf(w, aec.Bold.Apply("Press ENTER")+" to open your browser or submit your device code here: "+aec.Underline.Apply("%s\n"), strings.Split(state.VerificationURI, "?")[0]) + + tokenResChan := make(chan api.TokenResponse) + waitForTokenErrChan := make(chan error) + go func() { + tokenRes, err := m.api.WaitForDeviceToken(ctx, state) + if err != nil { + waitForTokenErrChan <- err + return + } + tokenResChan <- tokenRes + }() + + go func() { + reader := bufio.NewReader(os.Stdin) + _, _ = reader.ReadString('\n') + _ = m.openBrowser(state.VerificationURI) + }() + + _, _ = fmt.Fprint(w, "\nWaiting for authentication in the browser…\n") + var tokenRes api.TokenResponse + select { + case <-ctx.Done(): + return nil, errors.New("login canceled") + case err := <-waitForTokenErrChan: + return nil, fmt.Errorf("failed waiting for authentication: %w", err) + case tokenRes = <-tokenResChan: + } + + claims, err := oauth.GetClaims(tokenRes.AccessToken) + if err != nil { + return nil, fmt.Errorf("failed to parse token claims: %w", err) + } + + err = m.storeTokensInStore(tokenRes, claims.Domain.Username) + if err != nil { + return nil, fmt.Errorf("failed to store tokens: %w", err) + } + + pat, err := m.api.GetAutoPAT(ctx, m.audience, tokenRes) + if err != nil { + return nil, err + } + + return &types.AuthConfig{ + Username: claims.Domain.Username, + Password: pat, + ServerAddress: registry.IndexServer, + }, nil +} + +// Logout fetches the refresh token from the store and revokes it +// with the configured oauth tenant. The stored access and refresh +// tokens are then erased from the store. +// If the refresh token is not found in the store, an error is not +// returned. +func (m *OAuthManager) Logout(ctx context.Context) error { + refreshConfig, err := m.store.Get(refreshTokenKey) + if err != nil { + return err + } + if refreshConfig.Password == "" { + return nil + } + parts := strings.Split(refreshConfig.Password, "..") + if len(parts) != 2 { + // the token wasn't stored by the CLI, so don't revoke it + // or erase it from the store/error + return nil + } + // erase the token from the store first, that way + // if the revoke fails, the user can try to logout again + if err := m.eraseTokensFromStore(); err != nil { + return fmt.Errorf("failed to erase tokens: %w", err) + } + if err := m.api.RevokeToken(ctx, parts[0]); err != nil { + return fmt.Errorf("credentials erased successfully, but there was a failure to revoke the OAuth refresh token with the tenant: %w", err) + } + return nil +} + +const ( + accessTokenKey = registry.IndexServer + "access-token" + refreshTokenKey = registry.IndexServer + "refresh-token" +) + +func (m *OAuthManager) storeTokensInStore(tokens api.TokenResponse, username string) error { + return errors.Join( + m.store.Store(types.AuthConfig{ + Username: username, + Password: tokens.AccessToken, + ServerAddress: accessTokenKey, + }), + m.store.Store(types.AuthConfig{ + Username: username, + Password: tokens.RefreshToken + ".." + m.clientID, + ServerAddress: refreshTokenKey, + }), + ) +} + +func (m *OAuthManager) eraseTokensFromStore() error { + return errors.Join( + m.store.Erase(accessTokenKey), + m.store.Erase(refreshTokenKey), + ) +} diff --git a/cli/internal/oauth/manager/manager_test.go b/cli/internal/oauth/manager/manager_test.go new file mode 100644 index 000000000000..39a9b86c2fdb --- /dev/null +++ b/cli/internal/oauth/manager/manager_test.go @@ -0,0 +1,363 @@ +package manager + +import ( + "context" + "errors" + "os" + "testing" + "time" + + "github.com/docker/cli/cli/config/credentials" + "github.com/docker/cli/cli/config/types" + "github.com/docker/cli/cli/internal/oauth/api" + "gotest.tools/v3/assert" +) + +const ( + //nolint:lll + validToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InhYa3BCdDNyV3MyRy11YjlscEpncSJ9.eyJodHRwczovL2h1Yi5kb2NrZXIuY29tIjp7ImVtYWlsIjoiYm9ya0Bkb2NrZXIuY29tIiwic2Vzc2lvbl9pZCI6ImEtc2Vzc2lvbi1pZCIsInNvdXJjZSI6InNhbWxwIiwidXNlcm5hbWUiOiJib3JrISIsInV1aWQiOiIwMTIzLTQ1Njc4OSJ9LCJpc3MiOiJodHRwczovL2xvZ2luLmRvY2tlci5jb20vIiwic3ViIjoic2FtbHB8c2FtbHAtZG9ja2VyfGJvcmtAZG9ja2VyLmNvbSIsImF1ZCI6WyJodHRwczovL2F1ZGllbmNlLmNvbSJdLCJpYXQiOjE3MTk1MDI5MzksImV4cCI6MTcxOTUwNjUzOSwic2NvcGUiOiJvcGVuaWQgb2ZmbGluZV9hY2Nlc3MifQ.VUSp-9_SOvMPWJPRrSh7p4kSPoye4DA3kyd2I0TW0QtxYSRq7xCzNj0NC_ywlPlKBFBeXKm4mh93d1vBSh79I9Heq5tj0Fr4KH77U5xJRMEpjHqoT5jxMEU1hYXX92xctnagBMXxDvzUfu3Yf0tvYSA0RRoGbGTHfdYYRwOrGbwQ75Qg1dyIxUkwsG053eYX2XkmLGxymEMgIq_gWksgAamOc40_0OCdGr-MmDeD2HyGUa309aGltzQUw7Z0zG1AKSXy3WwfMHdWNFioTAvQphwEyY3US8ybSJi78upSFTjwUcryMeHUwQ3uV9PxwPMyPoYxo1izVB-OUJxM8RqEbg" +) + +// parsed token: +// { +// "https://hub.docker.com": { +// "email": "bork@docker.com", +// "session_id": "a-session-id", +// "source": "samlp", +// "username": "bork!", +// "uuid": "0123-456789" +// }, +// "iss": "https://login.docker.com/", +// "sub": "samlp|samlp-docker|bork@docker.com", +// "aud": [ +// "https://audience.com" +// ], +// "iat": 1719502939, +// "exp": 1719506539, +// "scope": "openid offline_access" +// } + +func TestLoginDevice(t *testing.T) { + t.Run("valid token", func(t *testing.T) { + expectedState := api.State{ + DeviceCode: "device-code", + UserCode: "0123-4567", + VerificationURI: "an-url", + ExpiresIn: 300, + } + var receivedAudience string + getDeviceToken := func(audience string) (api.State, error) { + receivedAudience = audience + return expectedState, nil + } + var receivedState api.State + waitForDeviceToken := func(state api.State) (api.TokenResponse, error) { + receivedState = state + return api.TokenResponse{ + AccessToken: validToken, + RefreshToken: "refresh-token", + }, nil + } + var receivedAccessToken, getPatReceivedAudience string + getAutoPat := func(audience string, res api.TokenResponse) (string, error) { + receivedAccessToken = res.AccessToken + getPatReceivedAudience = audience + return "a-pat", nil + } + api := &testAPI{ + getDeviceToken: getDeviceToken, + waitForDeviceToken: waitForDeviceToken, + getAutoPAT: getAutoPat, + } + store := newStore(map[string]types.AuthConfig{}) + manager := OAuthManager{ + store: credentials.NewFileStore(store), + audience: "https://hub.docker.com", + api: api, + openBrowser: func(url string) error { + return nil + }, + } + + authConfig, err := manager.LoginDevice(context.Background(), os.Stderr) + assert.NilError(t, err) + + assert.Equal(t, receivedAudience, "https://hub.docker.com") + assert.Equal(t, receivedState, expectedState) + assert.DeepEqual(t, authConfig, &types.AuthConfig{ + Username: "bork!", + Password: "a-pat", + ServerAddress: "https://index.docker.io/v1/", + }) + assert.Equal(t, receivedAccessToken, validToken) + assert.Equal(t, getPatReceivedAudience, "https://hub.docker.com") + }) + + t.Run("stores in cred store", func(t *testing.T) { + getDeviceToken := func(audience string) (api.State, error) { + return api.State{ + DeviceCode: "device-code", + UserCode: "0123-4567", + }, nil + } + waitForDeviceToken := func(state api.State) (api.TokenResponse, error) { + return api.TokenResponse{ + AccessToken: validToken, + RefreshToken: "refresh-token", + }, nil + } + getAutoPAT := func(audience string, res api.TokenResponse) (string, error) { + return "a-pat", nil + } + a := &testAPI{ + getDeviceToken: getDeviceToken, + waitForDeviceToken: waitForDeviceToken, + getAutoPAT: getAutoPAT, + } + store := newStore(map[string]types.AuthConfig{}) + manager := OAuthManager{ + clientID: "client-id", + store: credentials.NewFileStore(store), + api: a, + openBrowser: func(url string) error { + return nil + }, + } + + authConfig, err := manager.LoginDevice(context.Background(), os.Stderr) + assert.NilError(t, err) + + assert.Equal(t, authConfig.Password, "a-pat") + assert.Equal(t, authConfig.Username, "bork!") + + assert.Equal(t, len(store.configs), 2) + assert.Equal(t, store.configs["https://index.docker.io/v1/access-token"].Password, validToken) + assert.Equal(t, store.configs["https://index.docker.io/v1/refresh-token"].Password, "refresh-token..client-id") + }) + + t.Run("timeout", func(t *testing.T) { + getDeviceToken := func(audience string) (api.State, error) { + return api.State{ + DeviceCode: "device-code", + UserCode: "0123-4567", + VerificationURI: "an-url", + ExpiresIn: 300, + }, nil + } + waitForDeviceToken := func(state api.State) (api.TokenResponse, error) { + return api.TokenResponse{}, api.ErrTimeout + } + a := &testAPI{ + getDeviceToken: getDeviceToken, + waitForDeviceToken: waitForDeviceToken, + } + manager := OAuthManager{ + api: a, + openBrowser: func(url string) error { + return nil + }, + } + + _, err := manager.LoginDevice(context.Background(), os.Stderr) + assert.ErrorContains(t, err, "failed waiting for authentication: timed out waiting for device token") + }) + + t.Run("canceled context", func(t *testing.T) { + getDeviceToken := func(audience string) (api.State, error) { + return api.State{ + DeviceCode: "device-code", + UserCode: "0123-4567", + }, nil + } + waitForDeviceToken := func(state api.State) (api.TokenResponse, error) { + // make sure that the context is cancelled before this returns + time.Sleep(500 * time.Millisecond) + return api.TokenResponse{ + AccessToken: validToken, + RefreshToken: "refresh-token", + }, nil + } + a := &testAPI{ + getDeviceToken: getDeviceToken, + waitForDeviceToken: waitForDeviceToken, + } + manager := OAuthManager{ + api: a, + openBrowser: func(url string) error { + return nil + }, + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + _, err := manager.LoginDevice(ctx, os.Stderr) + assert.ErrorContains(t, err, "login canceled") + }) +} + +func TestLogout(t *testing.T) { + t.Run("successfully revokes token", func(t *testing.T) { + var receivedToken string + a := &testAPI{ + revokeToken: func(token string) error { + receivedToken = token + return nil + }, + } + store := newStore(map[string]types.AuthConfig{ + "https://index.docker.io/v1/access-token": { + Password: validToken, + }, + "https://index.docker.io/v1/refresh-token": { + Password: "a-refresh-token..client-id", + }, + }) + manager := OAuthManager{ + store: credentials.NewFileStore(store), + api: a, + } + + err := manager.Logout(context.Background()) + assert.NilError(t, err) + + assert.Equal(t, receivedToken, "a-refresh-token") + assert.Equal(t, len(store.configs), 0) + }) + + t.Run("error revoking token", func(t *testing.T) { + a := &testAPI{ + revokeToken: func(token string) error { + return errors.New("couldn't reach tenant") + }, + } + store := newStore(map[string]types.AuthConfig{ + "https://index.docker.io/v1/access-token": { + Password: validToken, + }, + "https://index.docker.io/v1/refresh-token": { + Password: "a-refresh-token..client-id", + }, + }) + manager := OAuthManager{ + store: credentials.NewFileStore(store), + api: a, + } + + err := manager.Logout(context.Background()) + assert.ErrorContains(t, err, "credentials erased successfully, but there was a failure to revoke the OAuth refresh token with the tenant: couldn't reach tenant") + + assert.Equal(t, len(store.configs), 0) + }) + + t.Run("invalid refresh token", func(t *testing.T) { + var triedRevoke bool + a := &testAPI{ + revokeToken: func(token string) error { + triedRevoke = true + return nil + }, + } + store := newStore(map[string]types.AuthConfig{ + "https://index.docker.io/v1/access-token": { + Password: validToken, + }, + "https://index.docker.io/v1/refresh-token": { + Password: "a-refresh-token-without-client-id", + }, + }) + manager := OAuthManager{ + store: credentials.NewFileStore(store), + api: a, + } + + err := manager.Logout(context.Background()) + assert.NilError(t, err) + + assert.Check(t, !triedRevoke) + }) + + t.Run("no refresh token", func(t *testing.T) { + a := &testAPI{} + var triedRevoke bool + revokeToken := func(token string) error { + triedRevoke = true + return nil + } + a.revokeToken = revokeToken + store := newStore(map[string]types.AuthConfig{}) + manager := OAuthManager{ + store: credentials.NewFileStore(store), + api: a, + } + + err := manager.Logout(context.Background()) + assert.NilError(t, err) + + assert.Check(t, !triedRevoke) + }) +} + +var _ api.OAuthAPI = &testAPI{} + +type testAPI struct { + getDeviceToken func(audience string) (api.State, error) + waitForDeviceToken func(state api.State) (api.TokenResponse, error) + refresh func(token string) (api.TokenResponse, error) + revokeToken func(token string) error + getAutoPAT func(audience string, res api.TokenResponse) (string, error) +} + +func (t *testAPI) GetDeviceCode(_ context.Context, audience string) (api.State, error) { + if t.getDeviceToken != nil { + return t.getDeviceToken(audience) + } + return api.State{}, nil +} + +func (t *testAPI) WaitForDeviceToken(_ context.Context, state api.State) (api.TokenResponse, error) { + if t.waitForDeviceToken != nil { + return t.waitForDeviceToken(state) + } + return api.TokenResponse{}, nil +} + +func (t *testAPI) Refresh(_ context.Context, token string) (api.TokenResponse, error) { + if t.refresh != nil { + return t.refresh(token) + } + return api.TokenResponse{}, nil +} + +func (t *testAPI) RevokeToken(_ context.Context, token string) error { + if t.revokeToken != nil { + return t.revokeToken(token) + } + return nil +} + +func (t *testAPI) GetAutoPAT(_ context.Context, audience string, res api.TokenResponse) (string, error) { + if t.getAutoPAT != nil { + return t.getAutoPAT(audience, res) + } + return "", nil +} + +type fakeStore struct { + configs map[string]types.AuthConfig +} + +func (f *fakeStore) Save() error { + return nil +} + +func (f *fakeStore) GetAuthConfigs() map[string]types.AuthConfig { + return f.configs +} + +func (f *fakeStore) GetFilename() string { + return "/tmp/docker-fakestore" +} + +func newStore(auths map[string]types.AuthConfig) *fakeStore { + return &fakeStore{configs: auths} +} diff --git a/cli/internal/oauth/manager/util.go b/cli/internal/oauth/manager/util.go new file mode 100644 index 000000000000..c5e268d21433 --- /dev/null +++ b/cli/internal/oauth/manager/util.go @@ -0,0 +1,28 @@ +package manager + +import ( + "fmt" + "runtime" + "strings" + + "github.com/docker/cli/cli/config/credentials" + "github.com/docker/cli/cli/version" +) + +const ( + audience = "https://hub.docker.com" + tenant = "login.docker.com" + clientID = "L4v0dmlNBpYUjGGab0C2JtgTgXr1Qz4d" +) + +func NewManager(store credentials.Store) *OAuthManager { + cliVersion := strings.ReplaceAll(version.Version, ".", "_") + options := OAuthManagerOptions{ + Store: store, + Audience: audience, + ClientID: clientID, + Tenant: tenant, + DeviceName: fmt.Sprintf("docker-cli:%s:%s-%s", cliVersion, runtime.GOOS, runtime.GOARCH), + } + return New(options) +} diff --git a/cli/required.go b/cli/required.go index 454e24761359..e8edcaafac2b 100644 --- a/cli/required.go +++ b/cli/required.go @@ -14,7 +14,7 @@ func NoArgs(cmd *cobra.Command, args []string) error { } if cmd.HasSubCommands() { - return errors.Errorf("\n" + strings.TrimRight(cmd.UsageString(), "\n")) + return errors.New("\n" + strings.TrimRight(cmd.UsageString(), "\n")) } return errors.Errorf( @@ -27,16 +27,16 @@ func NoArgs(cmd *cobra.Command, args []string) error { } // RequiresMinArgs returns an error if there is not at least min args -func RequiresMinArgs(min int) cobra.PositionalArgs { +func RequiresMinArgs(minArgs int) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { - if len(args) >= min { + if len(args) >= minArgs { return nil } return errors.Errorf( "%q requires at least %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s", cmd.CommandPath(), - min, - pluralize("argument", min), + minArgs, + pluralize("argument", minArgs), cmd.CommandPath(), cmd.UseLine(), cmd.Short, @@ -45,16 +45,16 @@ func RequiresMinArgs(min int) cobra.PositionalArgs { } // RequiresMaxArgs returns an error if there is not at most max args -func RequiresMaxArgs(max int) cobra.PositionalArgs { +func RequiresMaxArgs(maxArgs int) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { - if len(args) <= max { + if len(args) <= maxArgs { return nil } return errors.Errorf( "%q requires at most %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s", cmd.CommandPath(), - max, - pluralize("argument", max), + maxArgs, + pluralize("argument", maxArgs), cmd.CommandPath(), cmd.UseLine(), cmd.Short, @@ -63,17 +63,17 @@ func RequiresMaxArgs(max int) cobra.PositionalArgs { } // RequiresRangeArgs returns an error if there is not at least min args and at most max args -func RequiresRangeArgs(min int, max int) cobra.PositionalArgs { +func RequiresRangeArgs(minArgs int, maxArgs int) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { - if len(args) >= min && len(args) <= max { + if len(args) >= minArgs && len(args) <= maxArgs { return nil } return errors.Errorf( "%q requires at least %d and at most %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s", cmd.CommandPath(), - min, - max, - pluralize("argument", max), + minArgs, + maxArgs, + pluralize("argument", maxArgs), cmd.CommandPath(), cmd.UseLine(), cmd.Short, diff --git a/cli/required_test.go b/cli/required_test.go index eadc8645c426..9c5fc0b0c8b4 100644 --- a/cli/required_test.go +++ b/cli/required_test.go @@ -144,5 +144,6 @@ func newDummyCommand(validationFunc cobra.PositionalArgs) *cobra.Command { }, } cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) return cmd } diff --git a/cli/trust/trust.go b/cli/trust/trust.go index 745cede76f05..5e08b49ec544 100644 --- a/cli/trust/trust.go +++ b/cli/trust/trust.go @@ -157,7 +157,6 @@ func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo scope := auth.RepositoryScope{ Repository: repoInfo.Name.Name(), Actions: actions, - Class: repoInfo.Class, // TODO(thaJeztah): Class is no longer needed for plugins and can likely be removed; see https://github.com/docker/cli/pull/4114#discussion_r1145430825 } creds := simpleCredentialStore{auth: *authConfig} tokenHandlerOptions := auth.TokenHandlerOptions{ diff --git a/cmd/docker/builder_test.go b/cmd/docker/builder_test.go index 7de2572aa2b5..e78e1002806e 100644 --- a/cmd/docker/builder_test.go +++ b/cmd/docker/builder_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.21 +//go:build go1.22 package main diff --git a/cmd/docker/completions.go b/cmd/docker/completions.go index 582a2051e33d..95ee737b4276 100644 --- a/cmd/docker/completions.go +++ b/cmd/docker/completions.go @@ -1,31 +1,24 @@ package main import ( + "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/context/store" "github.com/spf13/cobra" ) -func registerCompletionFuncForGlobalFlags(contextStore store.Store, cmd *cobra.Command) error { - err := cmd.RegisterFlagCompletionFunc( - "context", - func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - names, err := store.Names(contextStore) - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - return names, cobra.ShellCompDirectiveNoFileComp - }, - ) +type contextStoreProvider interface { + ContextStore() store.Store +} + +func registerCompletionFuncForGlobalFlags(dockerCLI contextStoreProvider, cmd *cobra.Command) error { + err := cmd.RegisterFlagCompletionFunc("context", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + names, _ := store.Names(dockerCLI.ContextStore()) + return names, cobra.ShellCompDirectiveNoFileComp + }) if err != nil { return err } - err = cmd.RegisterFlagCompletionFunc( - "log-level", - func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - values := []string{"debug", "info", "warn", "error", "fatal"} - return values, cobra.ShellCompDirectiveNoFileComp - }, - ) + err = cmd.RegisterFlagCompletionFunc("log-level", completion.FromList("debug", "info", "warn", "error", "fatal")) if err != nil { return err } diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go index 7825c7d35455..23faa17365f6 100644 --- a/cmd/docker/docker.go +++ b/cmd/docker/docker.go @@ -102,7 +102,7 @@ func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand { cmd.SetErr(dockerCli.Err()) opts, helpCmd = cli.SetupRootCommand(cmd) - _ = registerCompletionFuncForGlobalFlags(dockerCli.ContextStore(), cmd) + _ = registerCompletionFuncForGlobalFlags(dockerCli, cmd) cmd.Flags().BoolP("version", "v", false, "Print version information and quit") setFlagErrorFunc(dockerCli, cmd) @@ -360,7 +360,11 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error { mp := dockerCli.MeterProvider() if mp, ok := mp.(command.MeterProvider); ok { - defer mp.Shutdown(ctx) + defer func() { + if err := mp.Shutdown(ctx); err != nil { + otel.Handle(err) + } + }() } else { fmt.Fprint(dockerCli.Err(), "Warning: Unexpected OTEL error, metrics may not be flushed") } diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index a934ecbbb683..08c6f0a542d8 100644 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -2591,7 +2591,6 @@ _docker_daemon() { --mtu --network-control-plane-mtu --node-generic-resource - --oom-score-adjust --pidfile -p --registry-mirror --seccomp-profile @@ -5081,7 +5080,7 @@ _docker_system_events() { return ;; daemon) - local name=$(__docker_q info | sed -n 's/^\(ID\|Name\): //p') + local name=$(__docker_q info --format '{{.Info.Name}} {{.Info.ID}}') COMPREPLY=( $( compgen -W "$name" -- "${cur##*=}" ) ) return ;; diff --git a/contrib/completion/zsh/_docker b/contrib/completion/zsh/_docker index 4236c225e375..333e981c25f7 100644 --- a/contrib/completion/zsh/_docker +++ b/contrib/completion/zsh/_docker @@ -2770,7 +2770,6 @@ __docker_subcommand() { "($help)--max-concurrent-uploads[Set the max concurrent uploads]" \ "($help)--max-download-attempts[Set the max download attempts for each pull]" \ "($help)--mtu=[Network MTU]:mtu:(0 576 1420 1500 9000)" \ - "($help)--oom-score-adjust=[Set the oom_score_adj for the daemon]:oom-score:(-500)" \ "($help -p --pidfile)"{-p=,--pidfile=}"[Path to use for daemon PID file]:PID file:_files" \ "($help)--raw-logs[Full timestamps without ANSI coloring]" \ "($help)*--registry-mirror=[Preferred registry mirror]:registry mirror: " \ diff --git a/docker-bake.hcl b/docker-bake.hcl index ef2f6c513a26..3d805cab1819 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -1,5 +1,5 @@ variable "GO_VERSION" { - default = "1.21.11" + default = "1.22.12" } variable "VERSION" { default = "" diff --git a/dockerfiles/Dockerfile.dev b/dockerfiles/Dockerfile.dev index 0fbf1d92d537..cd78b64549ed 100644 --- a/dockerfiles/Dockerfile.dev +++ b/dockerfiles/Dockerfile.dev @@ -1,9 +1,9 @@ # syntax=docker/dockerfile:1 -ARG GO_VERSION=1.21.11 +ARG GO_VERSION=1.22.12 ARG ALPINE_VERSION=3.20 -ARG BUILDX_VERSION=0.15.1 +ARG BUILDX_VERSION=0.17.1 FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang @@ -35,6 +35,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ FROM golang AS dev RUN apk add --no-cache \ bash \ + bash-completion \ build-base \ ca-certificates \ coreutils \ @@ -44,7 +45,8 @@ RUN apk add --no-cache \ nano RUN echo -e "\nYou are now in a development container. Run '\e\033[1mmake help\e\033[0m' to learn about\navailable make targets.\n" > /etc/motd \ - && echo -e "cat /etc/motd\nPS1=\"\e[0;32m\u@docker-cli-dev\\$ \e[0m\"" >> /root/.bashrc + && echo -e "cat /etc/motd\nPS1=\"\e[0;32m\u@docker-cli-dev\\$ \e[0m\"" >> /root/.bashrc \ + && echo -e "source /etc/bash/bash_completion.sh" >> /root/.bashrc CMD bash ENV DISABLE_WARN_OUTSIDE_CONTAINER=1 ENV PATH=$PATH:/go/src/github.com/docker/cli/build diff --git a/dockerfiles/Dockerfile.lint b/dockerfiles/Dockerfile.lint index cb040d62f40a..5516b0a124fb 100644 --- a/dockerfiles/Dockerfile.lint +++ b/dockerfiles/Dockerfile.lint @@ -1,8 +1,8 @@ # syntax=docker/dockerfile:1 -ARG GO_VERSION=1.21.11 +ARG GO_VERSION=1.22.12 ARG ALPINE_VERSION=3.20 -ARG GOLANGCI_LINT_VERSION=v1.59.0 +ARG GOLANGCI_LINT_VERSION=v1.62.0 FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint diff --git a/dockerfiles/Dockerfile.vendor b/dockerfiles/Dockerfile.vendor index f204ea273e19..bcb4cd538070 100644 --- a/dockerfiles/Dockerfile.vendor +++ b/dockerfiles/Dockerfile.vendor @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -ARG GO_VERSION=1.21.11 +ARG GO_VERSION=1.22.12 ARG ALPINE_VERSION=3.20 ARG MODOUTDATED_VERSION=v0.8.0 diff --git a/docs/deprecated.md b/docs/deprecated.md index e17879025dec..2748dd925492 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -1,4 +1,6 @@ --- +title: Deprecated Docker Engine features +linkTitle: Deprecated features aliases: ["/engine/misc/deprecated/"] description: "Deprecated Features." keywords: "docker, documentation, about, technology, deprecate" @@ -13,14 +15,12 @@ keywords: "docker, documentation, about, technology, deprecate" will be rejected. --> -# Deprecated Engine Features - This page provides an overview of features that are deprecated in Engine. Changes in packaging, and supported (Linux) distributions are not included. To learn about end of support for Linux distributions, refer to the [release notes](https://docs.docker.com/engine/release-notes/). -## Feature Deprecation Policy +## Feature deprecation policy As changes are made to Docker there may be times when existing features need to be removed or replaced with newer features. Before an existing feature is removed @@ -32,21 +32,24 @@ Users are expected to take note of the list of deprecated features each release and plan their migration away from those features, and (if applicable) towards the replacement features as soon as possible. -## Deprecated Engine Features +## Deprecated engine features -The table below provides an overview of the current status of deprecated features: +The following table provides an overview of the current status of deprecated features: - **Deprecated**: the feature is marked "deprecated" and should no longer be used. + The feature may be removed, disabled, or change behavior in a future release. - The _"Deprecated"_ column contains the release in which the feature was marked + The _"Deprecated"_ column contains the release in which the feature was marked deprecated, whereas the _"Remove"_ column contains a tentative release in which the feature is to be removed. If no release is included in the _"Remove"_ column, the release is yet to be decided on. -- **Removed**: the feature was removed, disabled, or hidden. Refer to the linked - section for details. Some features are "soft" deprecated, which means that they - remain functional for backward compatibility, and to allow users to migrate to - alternatives. In such cases, a warning may be printed, and users should not rely - on this feature. + +- **Removed**: the feature was removed, disabled, or hidden. + + Refer to the linked section for details. Some features are "soft" deprecated, + which means that they remain functional for backward compatibility, and to + allow users to migrate to alternatives. In such cases, a warning may be + printed, and users should not rely on this feature. | Status | Feature | Deprecated | Remove | |------------|------------------------------------------------------------------------------------------------------------------------------------|------------|--------| @@ -57,10 +60,10 @@ The table below provides an overview of the current status of deprecated feature | Deprecated | [`Container` and `ContainerConfig` fields in Image inspect](#container-and-containerconfig-fields-in-image-inspect) | v25.0 | v26.0 | | Deprecated | [Deprecate legacy API versions](#deprecate-legacy-api-versions) | v25.0 | v26.0 | | Removed | [Container short ID in network Aliases field](#container-short-id-in-network-aliases-field) | v25.0 | v26.0 | -| Deprecated | [IsAutomated field, and "is-automated" filter on docker search](#isautomated-field-and-is-automated-filter-on-docker-search) | v25.0 | v26.0 | +| Deprecated | [IsAutomated field, and `is-automated` filter on `docker search`](#isautomated-field-and-is-automated-filter-on-docker-search) | v25.0 | v26.0 | | Removed | [logentries logging driver](#logentries-logging-driver) | v24.0 | v25.0 | | Removed | [OOM-score adjust for the daemon](#oom-score-adjust-for-the-daemon) | v24.0 | v25.0 | -| Removed | [Buildkit build information](#buildkit-build-information) | v23.0 | v24.0 | +| Removed | [BuildKit build information](#buildkit-build-information) | v23.0 | v24.0 | | Deprecated | [Legacy builder for Linux images](#legacy-builder-for-linux-images) | v23.0 | - | | Deprecated | [Legacy builder fallback](#legacy-builder-fallback) | v23.0 | - | | Removed | [Btrfs storage driver on CentOS 7 and RHEL 7](#btrfs-storage-driver-on-centos-7-and-rhel-7) | v20.10 | v23.0 | @@ -91,7 +94,7 @@ The table below provides an overview of the current status of deprecated feature | Removed | [Asynchronous `service create` and `service update` as default](#asynchronous-service-create-and-service-update-as-default) | v17.05 | v17.10 | | Removed | [`-g` and `--graph` flags on `dockerd`](#-g-and---graph-flags-on-dockerd) | v17.05 | v23.0 | | Deprecated | [Top-level network properties in NetworkSettings](#top-level-network-properties-in-networksettings) | v1.13 | v17.12 | -| Removed | [`filter` param for `/images/json` endpoint](#filter-param-for-imagesjson-endpoint) | v1.13 | v20.10 | +| Removed | [`filter` option for `/images/json` endpoint](#filter-option-for-imagesjson-endpoint) | v1.13 | v20.10 | | Removed | [`repository:shortid` image references](#repositoryshortid-image-references) | v1.13 | v17.12 | | Removed | [`docker daemon` subcommand](#docker-daemon-subcommand) | v1.13 | v17.12 | | Removed | [Duplicate keys with conflicting values in engine labels](#duplicate-keys-with-conflicting-values-in-engine-labels) | v1.13 | v17.12 | @@ -122,8 +125,8 @@ The table below provides an overview of the current status of deprecated feature The `Config` field returned shown in `docker image inspect` (and as returned by the `GET /images/{name}/json` API endpoint) returns additional fields that are -not part of the image's configuration and not part of the [Docker Image Spec] -and [OCI Image Specification]. +not part of the image's configuration and not part of the [Docker image specification] +and [OCI image specification]. These fields are never set (and always return the default value for the type), but are not omitted in the response when left empty. As these fields were not @@ -131,7 +134,7 @@ intended to be part of the image configuration response, they are deprecated, and will be removed from the API in thee next release. The following fields are currently included in the API response, but are not -part of the underlying image's Config, and deprecated: +part of the underlying image's `Config` field, and deprecated: - `Hostname` - `Domainname` @@ -146,8 +149,8 @@ part of the underlying image's Config, and deprecated: - `MacAddress` (already omitted unless set) - `StopTimeout` (already omitted unless set) -[Docker image spec]: https://github.com/moby/docker-image-spec/blob/v1.3.1/specs-go/v1/image.go#L19-L32 -[OCI Image Spec]: https://github.com/opencontainers/image-spec/blob/v1.1.0/specs-go/v1/config.go#L24-L62 +[Docker image specification]: https://github.com/moby/docker-image-spec/blob/v1.3.1/specs-go/v1/image.go#L19-L32 +[OCI image specification]: https://github.com/opencontainers/image-spec/blob/v1.1.0/specs-go/v1/config.go#L24-L62 ### Graphdriver plugins (experimental) @@ -211,7 +214,7 @@ transit and providing a mechanism for mutual authentication. For environments remote daemon access isn't required, we recommend binding the Docker daemon to a Unix socket. -For daemon's where remote access is required and where TLS encryption is not feasible, +For daemons where remote access is required and where TLS encryption is not feasible, you may want to consider using SSH as an alternative solution. For further information, assistance, and step-by-step instructions on @@ -255,19 +258,19 @@ daemon may be updated to the latest release, but not all clients may be up-to-da or vice versa). Support for API versions before that (API versions provided by EOL versions of the Docker Daemon) is provided on a "best effort" basis. -Use of old API versions is very rare, and support for legacy API versions +Use of old API versions is rare, and support for legacy API versions involves significant complexity (Docker 1.0.0 having been released 10 years ago). Because of this, we'll start deprecating support for legacy API versions. Docker Engine v25.0 by default disables API version older than 1.24 (aligning the minimum supported API version between Linux and Windows daemons). When -connecting with a client that uses an API version version older than 1.24, -the daemon returns an error. The following example configures the docker +connecting with a client that uses an API version older than 1.24, +the daemon returns an error. The following example configures the Docker CLI to use API version 1.23, which produces an error: ```console DOCKER_API_VERSION=1.23 docker version -Error response from daemon: client version 1.23 is too old. Minimum supported API version is 1.24, +Error response from daemon: client version 1.23 is too old. Minimum supported API version is 1.24, upgrade your client to a newer version ``` @@ -301,12 +304,12 @@ A new field `DNSNames` containing the container name (if one was specified), the hostname, the network aliases, as well as the container short ID, has been introduced in v25.0 and should be used instead of the `Aliases` field. -### IsAutomated field, and "is-automated" filter on docker search +### IsAutomated field, and `is-automated` filter on `docker search` **Deprecated in Release: v25.0** **Target For Removal In Release: v26.0** -The "is_automated" field has been deprecated by Docker Hub's search API. +The `is_automated` field has been deprecated by Docker Hub's search API. Consequently, the `IsAutomated` field in image search will always be set to `false` in future, and searching for "is-automated=true" will yield no results. @@ -346,7 +349,7 @@ Users currently depending on this feature are recommended to adjust the daemon's OOM score using systemd or through other means, when starting the daemon. -### Buildkit build information +### BuildKit build information **Deprecated in Release: v23.0** **Removed in Release: v24.0** @@ -354,7 +357,7 @@ the daemon. [Build information](https://github.com/moby/buildkit/blob/v0.11/docs/buildinfo.md) structures have been introduced in [BuildKit v0.10.0](https://github.com/moby/buildkit/releases/tag/v0.10.0) and are generated with build metadata that allows you to see all the sources -(images, git repositories) that were used by the build with their exact +(images, Git repositories) that were used by the build with their exact versions and also the configuration that was passed to the build. This information is also embedded into the image configuration if one is generated. @@ -389,7 +392,7 @@ you to report issues in the [BuildKit issue tracker on GitHub](https://github.co > `docker build` continues to use the classic builder to build native Windows > images on Windows daemons. -### Legacy builder fallback +### Legacy builder fallback **Deprecated in Release: v23.0** @@ -402,11 +405,11 @@ To provide a smooth transition to BuildKit as the default builder, Docker v23.0 has an automatic fallback for some situations, or produces an error to assist users to resolve the problem. -In situations where the user did not explicitly opt-in to use BuildKit (i.e., +In situations where the user did not explicitly opt-in to use BuildKit (i.e., `DOCKER_BUILDKIT=1` is not set), the CLI automatically falls back to the classic builder, but prints a deprecation warning: -``` +```text DEPRECATED: The legacy builder is deprecated and will be removed in a future release. Install the buildx component to build images with BuildKit: https://docs.docker.com/go/buildx/ @@ -420,7 +423,7 @@ and use BuildKit for your builds, or opt-out of using BuildKit with `DOCKER_BUIL If you opted-in to use BuildKit (`DOCKER_BUILDKIT=1`), but the Buildx component is missing, an error is printed instead, and the `docker build` command fails: -``` +```text ERROR: BuildKit is enabled but the buildx component is missing or broken. Install the buildx component to build images with BuildKit: https://docs.docker.com/go/buildx/ @@ -470,27 +473,27 @@ to decrypt the private key, and store it un-encrypted to continue using it. Following the deprecation of [Compose on Kubernetes](https://github.com/docker/compose-on-kubernetes), support for Kubernetes in the `stack` and `context` commands has been removed from -the cli, and options related to this functionality are now either ignored, or may +the CLI, and options related to this functionality are now either ignored, or may produce an error. The following command-line flags are removed from the `docker context` subcommands: - `--default-stack-orchestrator` - swarm is now the only (and default) orchestrator for stacks. -- `--kubernetes` - the kubernetes endpoint can no longer be stored in `docker context`. +- `--kubernetes` - the Kubernetes endpoint can no longer be stored in `docker context`. - `--kubeconfig` - exporting a context as a kubeconfig file is no longer supported. The output produced by the `docker context inspect` subcommand no longer contains information about `StackOrchestrator` and `Kubernetes` endpoints for new contexts. The following command-line flags are removed from the `docker stack` subcommands: - + - `--kubeconfig` - using a kubeconfig file as context is no longer supported. -- `--namespace` - configuring the kubernetes namespace for stacks is no longer supported. +- `--namespace` - configuring the Kubernetes namespace for stacks is no longer supported. - `--orchestrator` - swarm is now the only (and default) orchestrator for stacks. The `DOCKER_STACK_ORCHESTRATOR`, `DOCKER_ORCHESTRATOR`, and `KUBECONFIG` environment variables, as well as the `stackOrchestrator` option in the `~/.docker/config.json` -cli configuration file are no longer used, and ignored. +CLI configuration file are no longer used, and ignored. ### Pulling images from non-compliant image registries @@ -532,7 +535,7 @@ major release. The experimental feature to run Linux containers on Windows (LCOW) was introduced as a technical preview in Docker 17.09. While many enhancements were made after its introduction, the feature never reached completeness, and development has -now stopped in favor of running docker natively on Linux in WSL2. +now stopped in favor of running Docker natively on Linux in WSL2. Developers who want to run Linux workloads on a Windows host are encouraged to use [Docker Desktop with WSL2](https://docs.docker.com/docker-for-windows/wsl/) instead. @@ -562,15 +565,14 @@ Docker API v1.42 and up now ignores this option when set. Older versions of the API continue to accept the option, but depending on the OCI runtime used, may take no effect. -> **Note** -> +> [!NOTE] > While not deprecated (yet) in Docker, the OCI runtime specification also > deprecated the `memory.kmem.tcp.limit_in_bytes` option. When using `runc` as -> runtime, this option takes no effect. The linux kernel did not explicitly +> runtime, this option takes no effect. The Linux kernel did not explicitly > deprecate this feature, and there is a tracking ticket in the `runc` issue > tracker to determine if this option should be reinstated or if this was an > oversight of the Linux kernel maintainers (see [opencontainers/runc#3174](https://github.com/opencontainers/runc/issues/3174)). -> +> > The `memory.kmem.tcp.limit_in_bytes` option is only supported with cgroups v1, > and not available on installations running with cgroups v2. This option is > only supported by the API, and not exposed on the `docker` command-line. @@ -589,7 +591,7 @@ networks using an external key/value store. The corresponding`--cluster-advertis **Deprecated in Release: v20.10** **Removed in Release: v23.0** -The docker CLI up until v1.7.0 used the `~/.dockercfg` file to store credentials +The Docker CLI up until v1.7.0 used the `~/.dockercfg` file to store credentials after authenticating to a registry (`docker login`). Docker v1.7.0 replaced this file with a new CLI configuration file, located in `~/.docker/config.json`. When implementing the new configuration file, the old file (and file-format) was kept @@ -723,8 +725,7 @@ to Docker Enterprise, using an image-based distribution of the Docker Engine. This feature was only available on Linux, and only when executed on a local node. Given the limitations of this feature, and the feature not getting widely adopted, the `docker engine` subcommands will be removed, in favor of installation through -standard package managers. - +standard package managers. ### Top-level `docker deploy` subcommand (experimental) @@ -737,7 +738,6 @@ The top-level `docker deploy` command (using the "Docker Application Bundle" 17.03, but superseded by support for Docker Compose files using the `docker stack deploy` subcommand. - ### `docker stack deploy` using "dab" files (experimental) **Deprecated in Release: v19.03** @@ -745,7 +745,7 @@ subcommand. **Removed in Release: v20.10** With no development being done on this feature, and no active use of the file -format, support for the DAB file format and the top-level docker deploy command +format, support for the DAB file format and the top-level `docker deploy` command (hidden by default in 19.03), will be removed, in favour of `docker stack deploy` using compose files. @@ -786,12 +786,12 @@ maintenance of the `aufs` storage driver. The `overlay` storage driver is deprecated in favor of the `overlay2` storage driver, which has all the benefits of `overlay`, without its limitations (excessive -inode consumption). The legacy `overlay` storage driver has been removed in +inode consumption). The legacy `overlay` storage driver has been removed in Docker Engine v24.0. Users of the `overlay` storage driver should migrate to the `overlay2` storage driver before upgrading to Docker Engine v24.0. The legacy `overlay` storage driver allowed using overlayFS-backed filesystems -on pre 4.x kernels. Now that all supported distributions are able to run `overlay2` +on kernels older than v4.x. Now that all supported distributions are able to run `overlay2` (as they are either on kernel 4.x, or have support for multiple lowerdirs backported), there is no reason to keep maintaining the `overlay` storage driver. @@ -825,7 +825,6 @@ were always documented to be reserved, but there was never any enforcement. Usage of these namespaces will now cause a warning in the engine logs to discourage their use, and will error instead in v20.10 and above. - ### `--disable-legacy-registry` override daemon option **Disabled In Release: v17.12** @@ -836,7 +835,6 @@ The `--disable-legacy-registry` flag was disabled in Docker 17.12 and will print an error when used. For this error to be printed, the flag itself is still present, but hidden. The flag has been removed in Docker 19.03. - ### Interacting with V1 registries **Disabled By Default In Release: v17.06** @@ -844,7 +842,7 @@ but hidden. The flag has been removed in Docker 19.03. **Removed In Release: v17.12** Version 1.8.3 added a flag (`--disable-legacy-registry=false`) which prevents the -docker daemon from `pull`, `push`, and `login` operations against v1 +Docker daemon from `pull`, `push`, and `login` operations against v1 registries. Though enabled by default, this signals the intent to deprecate the v1 protocol. @@ -856,7 +854,6 @@ Starting with Docker 17.12, support for V1 registries has been removed, and the `--disable-legacy-registry` flag can no longer be used, and `dockerd` will fail to start when set. - ### Asynchronous `service create` and `service update` as default **Deprecated In Release: v17.05** @@ -896,20 +893,22 @@ about the default ("bridge") network; These properties are deprecated in favor of per-network properties in `NetworkSettings.Networks`. These properties were already "deprecated" in -docker 1.9, but kept around for backward compatibility. +Docker 1.9, but kept around for backward compatibility. Refer to [#17538](https://github.com/docker/docker/pull/17538) for further information. -### `filter` param for `/images/json` endpoint +### `filter` option for `/images/json` endpoint + **Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** **Removed In Release: v20.10** -The `filter` param to filter the list of image by reference (name or name:tag) +The `filter` option to filter the list of image by reference (name or name:tag) is now implemented as a regular filter, named `reference`. ### `repository:shortid` image references + **Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** **Removed In Release: v17.12** @@ -921,6 +920,7 @@ Support for the `repository:shortid` notation to reference images was removed in Docker 17.12. ### `docker daemon` subcommand + **Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** **Removed In Release: v17.12** @@ -928,6 +928,7 @@ in Docker 17.12. The daemon is moved to a separate binary (`dockerd`), and should be used instead. ### Duplicate keys with conflicting values in engine labels + **Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** **Removed In Release: v17.12** @@ -936,11 +937,13 @@ When setting duplicate keys with conflicting values, an error will be produced, will fail to start. ### `MAINTAINER` in Dockerfile + **Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** `MAINTAINER` was an early very limited form of `LABEL` which should be used instead. ### API calls without a version + **Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** **Target For Removal In Release: v17.12** @@ -950,6 +953,7 @@ future Engine versions. Instead of just requesting, for example, the URL `/containers/json`, you must now request `/v1.25/containers/json`. ### Backing filesystem without `d_type` support for overlay/overlay2 + **Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** **Removed In Release: v17.12** @@ -964,7 +968,6 @@ backing filesystem without `d_type` support. Refer to [#27358](https://github.com/docker/docker/issues/27358) for details. - ### `--automated` and `--stars` flags on `docker search` **Deprecated in Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** @@ -974,7 +977,6 @@ Refer to [#27358](https://github.com/docker/docker/issues/27358) for details. The `docker search --automated` and `docker search --stars` options are deprecated. Use `docker search --filter=is-automated=` and `docker search --filter=stars=...` instead. - ### `-h` shorthand for `--help` **Deprecated In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** @@ -987,13 +989,15 @@ on all subcommands (due to it conflicting with, e.g. `-h` / `--hostname` on "usage" output of subcommands, nor documented, and is now marked "deprecated". ### `-e` and `--email` flags on `docker login` + **Deprecated In Release: [v1.11.0](https://github.com/docker/docker/releases/tag/v1.11.0)** **Removed In Release: [v17.06](https://github.com/docker/docker-ce/releases/tag/v17.06.0-ce)** -The docker login command is removing the ability to automatically register for an account with the target registry if the given username doesn't exist. Due to this change, the email flag is no longer required, and will be deprecated. +The `docker login` no longer automatically registers an account with the target registry if the given username doesn't exist. Due to this change, the email flag is no longer required, and will be deprecated. ### Separator (`:`) of `--security-opt` flag on `docker run` + **Deprecated In Release: [v1.11.0](https://github.com/docker/docker/releases/tag/v1.11.0)** **Target For Removal In Release: v17.06** @@ -1001,12 +1005,14 @@ The docker login command is removing the ability to automatically register for a The flag `--security-opt` doesn't use the colon separator (`:`) anymore to divide keys and values, it uses the equal symbol (`=`) for consistency with other similar flags, like `--storage-opt`. ### Ambiguous event fields in API + **Deprecated In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** The fields `ID`, `Status` and `From` in the events API have been deprecated in favor of a more rich structure. See the events API documentation for the new format. ### `-f` flag on `docker tag` + **Deprecated In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** **Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** @@ -1014,6 +1020,7 @@ See the events API documentation for the new format. To make tagging consistent across the various `docker` commands, the `-f` flag on the `docker tag` command is deprecated. It is no longer necessary to specify `-f` to move a tag from one image to another. Nor will `docker` generate an error if the `-f` flag is missing and the specified tag is already in use. ### HostConfig at API container start + **Deprecated In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** **Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** @@ -1030,8 +1037,8 @@ defining it at container creation (`POST /containers/create`). The `docker ps --before` and `docker ps --since` options are deprecated. Use `docker ps --filter=before=...` and `docker ps --filter=since=...` instead. - ### Driver-specific log tags + **Deprecated In Release: [v1.9.0](https://github.com/docker/docker/releases/tag/v1.9.0)** **Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** @@ -1044,8 +1051,8 @@ Because of which, the driver specific log tag options `syslog-tag`, `gelf-tag` a $ docker --log-driver=syslog --log-opt tag="{{.ImageName}}/{{.Name}}/{{.ID}}" ``` - ### Docker Content Trust ENV passphrase variables name change + **Deprecated In Release: [v1.9.0](https://github.com/docker/docker/releases/tag/v1.9.0)** **Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** @@ -1055,7 +1062,6 @@ Since 1.9, Docker Content Trust Offline key has been renamed to Root key and the - DOCKER_CONTENT_TRUST_OFFLINE_PASSPHRASE is now named DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE - DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE is now named DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE - ### `/containers/(id or name)/copy` endpoint **Deprecated In Release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)** @@ -1064,63 +1070,61 @@ Since 1.9, Docker Content Trust Offline key has been renamed to Root key and the The endpoint `/containers/(id or name)/copy` is deprecated in favor of `/containers/(id or name)/archive`. - ### LXC built-in exec driver + **Deprecated In Release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)** **Removed In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** The built-in LXC execution driver, the lxc-conf flag, and API fields have been removed. - ### Old Command Line Options + **Deprecated In Release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)** **Removed In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** -The flags `-d` and `--daemon` are deprecated in favor of the `daemon` subcommand: - - docker daemon -H ... +The flags `-d` and `--daemon` are deprecated. Use the separate `dockerd` binary instead. The following single-dash (`-opt`) variant of certain command line options are deprecated and replaced with double-dash options (`--opt`): - docker attach -nostdin - docker attach -sig-proxy - docker build -no-cache - docker build -rm - docker commit -author - docker commit -run - docker events -since - docker history -notrunc - docker images -notrunc - docker inspect -format - docker ps -beforeId - docker ps -notrunc - docker ps -sinceId - docker rm -link - docker run -cidfile - docker run -dns - docker run -entrypoint - docker run -expose - docker run -link - docker run -lxc-conf - docker run -n - docker run -privileged - docker run -volumes-from - docker search -notrunc - docker search -stars - docker search -t - docker search -trusted - docker tag -force +- `docker attach -nostdin` +- `docker attach -sig-proxy` +- `docker build -no-cache` +- `docker build -rm` +- `docker commit -author` +- `docker commit -run` +- `docker events -since` +- `docker history -notrunc` +- `docker images -notrunc` +- `docker inspect -format` +- `docker ps -beforeId` +- `docker ps -notrunc` +- `docker ps -sinceId` +- `docker rm -link` +- `docker run -cidfile` +- `docker run -dns` +- `docker run -entrypoint` +- `docker run -expose` +- `docker run -link` +- `docker run -lxc-conf` +- `docker run -n` +- `docker run -privileged` +- `docker run -volumes-from` +- `docker search -notrunc` +- `docker search -stars` +- `docker search -t` +- `docker search -trusted` +- `docker tag -force` The following double-dash options are deprecated and have no replacement: - docker run --cpuset - docker run --networking - docker ps --since-id - docker ps --before-id - docker search --trusted +- `docker run --cpuset` +- `docker run --networking` +- `docker ps --since-id` +- `docker ps --before-id` +- `docker search --trusted` **Deprecated In Release: [v1.5.0](https://github.com/docker/docker/releases/tag/v1.5.0)** @@ -1128,11 +1132,7 @@ The following double-dash options are deprecated and have no replacement: The single-dash (`-help`) was removed, in favor of the double-dash `--help` - docker -help - docker [COMMAND] -help - - -### `--api-enable-cors` flag on dockerd +### `--api-enable-cors` flag on `dockerd` **Deprecated In Release: [v1.6.0](https://github.com/docker/docker/releases/tag/v1.6.0)** @@ -1141,19 +1141,19 @@ The single-dash (`-help`) was removed, in favor of the double-dash `--help` The flag `--api-enable-cors` is deprecated since v1.6.0. Use the flag `--api-cors-header` instead. -### `--run` flag on docker commit +### `--run` flag on `docker commit` **Deprecated In Release: [v0.10.0](https://github.com/docker/docker/releases/tag/v0.10.0)** **Removed In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** -The flag `--run` of the docker commit (and its short version `-run`) were deprecated in favor +The flag `--run` of the `docker commit` command (and its short version `-run`) were deprecated in favor of the `--changes` flag that allows to pass `Dockerfile` commands. - ### Three arguments form in `docker import` + **Deprecated In Release: [v0.6.7](https://github.com/docker/docker/releases/tag/v0.6.7)** **Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** -The `docker import` command format `file|URL|- [REPOSITORY [TAG]]` is deprecated since November 2013. It's no more supported. +The `docker import` command format `file|URL|- [REPOSITORY [TAG]]` is deprecated since November 2013. It's no longer supported. diff --git a/docs/extend/index.md b/docs/extend/_index.md similarity index 98% rename from docs/extend/index.md rename to docs/extend/_index.md index c90a42cb4f7e..57528444d1ee 100644 --- a/docs/extend/index.md +++ b/docs/extend/_index.md @@ -1,5 +1,6 @@ --- title: Docker Engine managed plugin system +linkTitle: Docker Engine plugins description: Develop and use a plugin with the managed plugin system keywords: "API, Usage, plugins, documentation, developer" aliases: @@ -16,8 +17,7 @@ plugins using Docker Engine. For information about legacy (non-managed) plugins, refer to [Understand legacy Docker Engine plugins](legacy_plugins.md). -> **Note** -> +> [!NOTE] > Docker Engine managed plugins are currently not supported on Windows daemons. ## Installing and using a plugin @@ -38,8 +38,7 @@ operation, such as creating a volume. In the following example, you install the `sshfs` plugin, verify that it is enabled, and use it to create a volume. -> **Note** -> +> [!NOTE] > This example is intended for instructional purposes only. Once the volume is > created, your SSH password to the remote host is exposed as plaintext when > inspecting the volume. Delete the volume as soon as you are done with the @@ -117,7 +116,7 @@ enabled, and use it to create a volume. To disable a plugin, use the `docker plugin disable` command. To completely remove it, use the `docker plugin remove` command. For other available commands and options, see the -[command line reference](https://docs.docker.com/engine/reference/commandline/cli/). +[command line reference](https://docs.docker.com/reference/cli/docker/). ## Developing a plugin @@ -126,8 +125,7 @@ commands and options, see the The `rootfs` directory represents the root filesystem of the plugin. In this example, it was created from a Dockerfile: -> **Note** -> +> [!NOTE] > The `/run/docker/plugins` directory is mandatory inside of the > plugin's filesystem for Docker to communicate with the plugin. diff --git a/docs/extend/legacy_plugins.md b/docs/extend/legacy_plugins.md index 830a7dce9c27..2c6ba4bc1a2e 100644 --- a/docs/extend/legacy_plugins.md +++ b/docs/extend/legacy_plugins.md @@ -8,7 +8,7 @@ keywords: "Examples, Usage, plugins, docker, documentation, user guide" This document describes the Docker Engine plugins generally available in Docker Engine. To view information on plugins managed by Docker, -refer to [Docker Engine plugin system](index.md). +refer to [Docker Engine plugin system](_index.md). You can extend the capabilities of the Docker Engine by loading third-party plugins. This page explains the types of plugins and provides links to several @@ -16,7 +16,7 @@ volume and network plugins for Docker. ## Types of plugins -Plugins extend Docker's functionality. They come in specific types. For +Plugins extend Docker's functionality. They come in specific types. For example, a [volume plugin](plugins_volume.md) might enable Docker volumes to persist across multiple Docker hosts and a [network plugin](plugins_network.md) might provide network plumbing. diff --git a/docs/extend/plugin_api.md b/docs/extend/plugin_api.md index 4600992c4e1c..a0b0a7367552 100644 --- a/docs/extend/plugin_api.md +++ b/docs/extend/plugin_api.md @@ -8,7 +8,7 @@ Docker plugins are out-of-process extensions which add capabilities to the Docker Engine. This document describes the Docker Engine plugin API. To view information on -plugins managed by Docker Engine, refer to [Docker Engine plugin system](index.md). +plugins managed by Docker Engine, refer to [Docker Engine plugin system](_index.md). This page is intended for people who want to develop their own Docker plugin. If you just want to learn about or use Docker plugins, look diff --git a/docs/extend/plugins_authorization.md b/docs/extend/plugins_authorization.md index 79201ec2367e..b8678debd59d 100644 --- a/docs/extend/plugins_authorization.md +++ b/docs/extend/plugins_authorization.md @@ -8,7 +8,7 @@ aliases: This document describes the Docker Engine plugins available in Docker Engine. To view information on plugins managed by Docker Engine, -refer to [Docker Engine plugin system](index.md). +refer to [Docker Engine plugin system](_index.md). Docker's out-of-the-box authorization model is all or nothing. Any user with permission to access the Docker daemon can run any Docker client command. The @@ -43,8 +43,7 @@ Authorization plugins must follow the rules described in [Docker Plugin API](plu Each plugin must reside within directories described under the [Plugin discovery](plugin_api.md#plugin-discovery) section. -> **Note** -> +> [!NOTE] > The abbreviations `AuthZ` and `AuthN` mean authorization and authentication > respectively. diff --git a/docs/extend/plugins_metrics.md b/docs/extend/plugins_metrics.md index dc47e4692670..a744406f7d3e 100644 --- a/docs/extend/plugins_metrics.md +++ b/docs/extend/plugins_metrics.md @@ -8,8 +8,7 @@ Docker exposes internal metrics based on the Prometheus format. Metrics plugins enable accessing these metrics in a consistent way by providing a Unix socket at a predefined path where the plugin can scrape the metrics. -> **Note** -> +> [!NOTE] > While the plugin interface for metrics is non-experimental, the naming of the > metrics and metric labels is still considered experimental and may change in a > future version. diff --git a/docs/extend/plugins_network.md b/docs/extend/plugins_network.md index e24fd2625702..8f94546b01b4 100644 --- a/docs/extend/plugins_network.md +++ b/docs/extend/plugins_network.md @@ -6,12 +6,12 @@ keywords: "Examples, Usage, plugins, docker, documentation, user guide" This document describes Docker Engine network driver plugins generally available in Docker Engine. To view information on plugins -managed by Docker Engine, refer to [Docker Engine plugin system](index.md). +managed by Docker Engine, refer to [Docker Engine plugin system](_index.md). Docker Engine network plugins enable Engine deployments to be extended to support a wide range of networking technologies, such as VXLAN, IPVLAN, MACVLAN or something completely different. Network driver plugins are supported via the -LibNetwork project. Each plugin is implemented as a "remote driver" for +LibNetwork project. Each plugin is implemented as a "remote driver" for LibNetwork, which shares plugin infrastructure with Engine. Effectively, network driver plugins are activated in the same way as other plugins, and use the same kind of protocol. @@ -19,7 +19,7 @@ kind of protocol. ## Network plugins and Swarm mode [Legacy plugins](legacy_plugins.md) do not work in Swarm mode. However, -plugins written using the [v2 plugin system](index.md) do work in Swarm mode, as +plugins written using the [v2 plugin system](_index.md) do work in Swarm mode, as long as they are installed on each Swarm worker node. ## Use network driver plugins diff --git a/docs/extend/plugins_volume.md b/docs/extend/plugins_volume.md index 3e42bc75aafd..8da7deb742c9 100644 --- a/docs/extend/plugins_volume.md +++ b/docs/extend/plugins_volume.md @@ -80,8 +80,7 @@ provide the Docker Daemon with writeable paths on the host filesystem. The Docke daemon provides these paths to containers to consume. The Docker daemon makes the volumes available by bind-mounting the provided paths into the containers. -> **Note** -> +> [!NOTE] > Volume plugins should *not* write data to the `/var/lib/docker/` directory, > including `/var/lib/docker/volumes`. The `/var/lib/docker/` directory is > reserved for Docker. diff --git a/docs/generate/go.mod b/docs/generate/go.mod index d62ff455713a..451d2965338c 100644 --- a/docs/generate/go.mod +++ b/docs/generate/go.mod @@ -3,11 +3,11 @@ module github.com/docker/cli/docs/generate // dummy go.mod to avoid dealing with dependencies specific // to docs generation and not really part of the project. -go 1.16 +go 1.22.0 //require ( // github.com/docker/cli v0.0.0+incompatible -// github.com/docker/cli-docs-tool v0.5.0 +// github.com/docker/cli-docs-tool v0.8.0 //) // //replace github.com/docker/cli v0.0.0+incompatible => ../../ diff --git a/docs/reference/commandline/attach.md b/docs/reference/commandline/attach.md index 3a89574075b9..09ceeeaca785 100644 --- a/docs/reference/commandline/attach.md +++ b/docs/reference/commandline/attach.md @@ -12,7 +12,7 @@ Attach local standard input, output, and error streams to a running container | Name | Type | Default | Description | |:----------------|:---------|:--------|:----------------------------------------------------| | `--detach-keys` | `string` | | Override the key sequence for detaching a container | -| `--no-stdin` | | | Do not attach STDIN | +| `--no-stdin` | `bool` | | Do not attach STDIN | | `--sig-proxy` | `bool` | `true` | Proxy all received signals to the process | diff --git a/docs/reference/commandline/build.md b/docs/reference/commandline/build.md index c0dd7a676897..dca5ee76ab84 100644 --- a/docs/reference/commandline/build.md +++ b/docs/reference/commandline/build.md @@ -5,42 +5,42 @@ Build an image from a Dockerfile ### Aliases -`docker image build`, `docker build`, `docker buildx build`, `docker builder build` +`docker image build`, `docker build`, `docker builder build` ### Options -| Name | Type | Default | Description | -|:--------------------------|:--------------|:----------|:------------------------------------------------------------------| -| `--add-host` | `list` | | Add a custom host-to-IP mapping (`host:ip`) | -| `--build-arg` | `list` | | Set build-time variables | -| `--cache-from` | `stringSlice` | | Images to consider as cache sources | -| `--cgroup-parent` | `string` | | Set the parent cgroup for the `RUN` instructions during build | -| `--compress` | | | Compress the build context using gzip | -| `--cpu-period` | `int64` | `0` | Limit the CPU CFS (Completely Fair Scheduler) period | -| `--cpu-quota` | `int64` | `0` | Limit the CPU CFS (Completely Fair Scheduler) quota | -| `-c`, `--cpu-shares` | `int64` | `0` | CPU shares (relative weight) | -| `--cpuset-cpus` | `string` | | CPUs in which to allow execution (0-3, 0,1) | -| `--cpuset-mems` | `string` | | MEMs in which to allow execution (0-3, 0,1) | -| `--disable-content-trust` | `bool` | `true` | Skip image verification | -| `-f`, `--file` | `string` | | Name of the Dockerfile (Default is `PATH/Dockerfile`) | -| `--force-rm` | | | Always remove intermediate containers | -| `--iidfile` | `string` | | Write the image ID to the file | -| `--isolation` | `string` | | Container isolation technology | -| `--label` | `list` | | Set metadata for an image | -| `-m`, `--memory` | `bytes` | `0` | Memory limit | -| `--memory-swap` | `bytes` | `0` | Swap limit equal to memory plus swap: -1 to enable unlimited swap | -| `--network` | `string` | `default` | Set the networking mode for the RUN instructions during build | -| `--no-cache` | | | Do not use cache when building the image | -| `--platform` | `string` | | Set platform if server is multi-platform capable | -| `--pull` | | | Always attempt to pull a newer version of the image | -| `-q`, `--quiet` | | | Suppress the build output and print image ID on success | -| `--rm` | `bool` | `true` | Remove intermediate containers after a successful build | -| `--security-opt` | `stringSlice` | | Security options | -| `--shm-size` | `bytes` | `0` | Size of `/dev/shm` | -| `--squash` | | | Squash newly built layers into a single new layer | -| `-t`, `--tag` | `list` | | Name and optionally a tag in the `name:tag` format | -| `--target` | `string` | | Set the target build stage to build. | -| `--ulimit` | `ulimit` | | Ulimit options | +| Name | Type | Default | Description | +|:-----------------------------------------------------------------------------------------------------------------------------------------------------|:--------------|:----------|:------------------------------------------------------------------| +| [`--add-host`](https://docs.docker.com/reference/cli/docker/buildx/build/#add-host) | `list` | | Add a custom host-to-IP mapping (`host:ip`) | +| [`--build-arg`](https://docs.docker.com/reference/cli/docker/buildx/build/#build-arg) | `list` | | Set build-time variables | +| `--cache-from` | `stringSlice` | | Images to consider as cache sources | +| [`--cgroup-parent`](https://docs.docker.com/reference/cli/docker/buildx/build/#cgroup-parent) | `string` | | Set the parent cgroup for the `RUN` instructions during build | +| `--compress` | `bool` | | Compress the build context using gzip | +| `--cpu-period` | `int64` | `0` | Limit the CPU CFS (Completely Fair Scheduler) period | +| `--cpu-quota` | `int64` | `0` | Limit the CPU CFS (Completely Fair Scheduler) quota | +| `-c`, `--cpu-shares` | `int64` | `0` | CPU shares (relative weight) | +| `--cpuset-cpus` | `string` | | CPUs in which to allow execution (0-3, 0,1) | +| `--cpuset-mems` | `string` | | MEMs in which to allow execution (0-3, 0,1) | +| `--disable-content-trust` | `bool` | `true` | Skip image verification | +| [`-f`](https://docs.docker.com/reference/cli/docker/buildx/build/#file), [`--file`](https://docs.docker.com/reference/cli/docker/buildx/build/#file) | `string` | | Name of the Dockerfile (Default is `PATH/Dockerfile`) | +| `--force-rm` | `bool` | | Always remove intermediate containers | +| `--iidfile` | `string` | | Write the image ID to the file | +| `--isolation` | `string` | | Container isolation technology | +| `--label` | `list` | | Set metadata for an image | +| `-m`, `--memory` | `bytes` | `0` | Memory limit | +| `--memory-swap` | `bytes` | `0` | Swap limit equal to memory plus swap: -1 to enable unlimited swap | +| [`--network`](https://docs.docker.com/reference/cli/docker/buildx/build/#network) | `string` | `default` | Set the networking mode for the RUN instructions during build | +| `--no-cache` | `bool` | | Do not use cache when building the image | +| `--platform` | `string` | | Set platform if server is multi-platform capable | +| `--pull` | `bool` | | Always attempt to pull a newer version of the image | +| `-q`, `--quiet` | `bool` | | Suppress the build output and print image ID on success | +| `--rm` | `bool` | `true` | Remove intermediate containers after a successful build | +| `--security-opt` | `stringSlice` | | Security options | +| `--shm-size` | `bytes` | `0` | Size of `/dev/shm` | +| `--squash` | `bool` | | Squash newly built layers into a single new layer | +| [`-t`](https://docs.docker.com/reference/cli/docker/buildx/build/#tag), [`--tag`](https://docs.docker.com/reference/cli/docker/buildx/build/#tag) | `list` | | Name and optionally a tag in the `name:tag` format | +| [`--target`](https://docs.docker.com/reference/cli/docker/buildx/build/#target) | `string` | | Set the target build stage to build. | +| `--ulimit` | `ulimit` | | Ulimit options | diff --git a/docs/reference/commandline/builder_build.md b/docs/reference/commandline/builder_build.md index b972d573fac1..ad9c09532159 100644 --- a/docs/reference/commandline/builder_build.md +++ b/docs/reference/commandline/builder_build.md @@ -5,42 +5,42 @@ Build an image from a Dockerfile ### Aliases -`docker image build`, `docker build`, `docker buildx build`, `docker builder build` +`docker image build`, `docker build`, `docker builder build` ### Options -| Name | Type | Default | Description | -|:--------------------------|:--------------|:----------|:------------------------------------------------------------------| -| `--add-host` | `list` | | Add a custom host-to-IP mapping (`host:ip`) | -| `--build-arg` | `list` | | Set build-time variables | -| `--cache-from` | `stringSlice` | | Images to consider as cache sources | -| `--cgroup-parent` | `string` | | Set the parent cgroup for the `RUN` instructions during build | -| `--compress` | | | Compress the build context using gzip | -| `--cpu-period` | `int64` | `0` | Limit the CPU CFS (Completely Fair Scheduler) period | -| `--cpu-quota` | `int64` | `0` | Limit the CPU CFS (Completely Fair Scheduler) quota | -| `-c`, `--cpu-shares` | `int64` | `0` | CPU shares (relative weight) | -| `--cpuset-cpus` | `string` | | CPUs in which to allow execution (0-3, 0,1) | -| `--cpuset-mems` | `string` | | MEMs in which to allow execution (0-3, 0,1) | -| `--disable-content-trust` | `bool` | `true` | Skip image verification | -| `-f`, `--file` | `string` | | Name of the Dockerfile (Default is `PATH/Dockerfile`) | -| `--force-rm` | | | Always remove intermediate containers | -| `--iidfile` | `string` | | Write the image ID to the file | -| `--isolation` | `string` | | Container isolation technology | -| `--label` | `list` | | Set metadata for an image | -| `-m`, `--memory` | `bytes` | `0` | Memory limit | -| `--memory-swap` | `bytes` | `0` | Swap limit equal to memory plus swap: -1 to enable unlimited swap | -| `--network` | `string` | `default` | Set the networking mode for the RUN instructions during build | -| `--no-cache` | | | Do not use cache when building the image | -| `--platform` | `string` | | Set platform if server is multi-platform capable | -| `--pull` | | | Always attempt to pull a newer version of the image | -| `-q`, `--quiet` | | | Suppress the build output and print image ID on success | -| `--rm` | `bool` | `true` | Remove intermediate containers after a successful build | -| `--security-opt` | `stringSlice` | | Security options | -| `--shm-size` | `bytes` | `0` | Size of `/dev/shm` | -| `--squash` | | | Squash newly built layers into a single new layer | -| `-t`, `--tag` | `list` | | Name and optionally a tag in the `name:tag` format | -| `--target` | `string` | | Set the target build stage to build. | -| `--ulimit` | `ulimit` | | Ulimit options | +| Name | Type | Default | Description | +|:-----------------------------------------------------------------------------------------------------------------------------------------------------|:--------------|:----------|:------------------------------------------------------------------| +| [`--add-host`](https://docs.docker.com/reference/cli/docker/buildx/build/#add-host) | `list` | | Add a custom host-to-IP mapping (`host:ip`) | +| [`--build-arg`](https://docs.docker.com/reference/cli/docker/buildx/build/#build-arg) | `list` | | Set build-time variables | +| `--cache-from` | `stringSlice` | | Images to consider as cache sources | +| [`--cgroup-parent`](https://docs.docker.com/reference/cli/docker/buildx/build/#cgroup-parent) | `string` | | Set the parent cgroup for the `RUN` instructions during build | +| `--compress` | `bool` | | Compress the build context using gzip | +| `--cpu-period` | `int64` | `0` | Limit the CPU CFS (Completely Fair Scheduler) period | +| `--cpu-quota` | `int64` | `0` | Limit the CPU CFS (Completely Fair Scheduler) quota | +| `-c`, `--cpu-shares` | `int64` | `0` | CPU shares (relative weight) | +| `--cpuset-cpus` | `string` | | CPUs in which to allow execution (0-3, 0,1) | +| `--cpuset-mems` | `string` | | MEMs in which to allow execution (0-3, 0,1) | +| `--disable-content-trust` | `bool` | `true` | Skip image verification | +| [`-f`](https://docs.docker.com/reference/cli/docker/buildx/build/#file), [`--file`](https://docs.docker.com/reference/cli/docker/buildx/build/#file) | `string` | | Name of the Dockerfile (Default is `PATH/Dockerfile`) | +| `--force-rm` | `bool` | | Always remove intermediate containers | +| `--iidfile` | `string` | | Write the image ID to the file | +| `--isolation` | `string` | | Container isolation technology | +| `--label` | `list` | | Set metadata for an image | +| `-m`, `--memory` | `bytes` | `0` | Memory limit | +| `--memory-swap` | `bytes` | `0` | Swap limit equal to memory plus swap: -1 to enable unlimited swap | +| [`--network`](https://docs.docker.com/reference/cli/docker/buildx/build/#network) | `string` | `default` | Set the networking mode for the RUN instructions during build | +| `--no-cache` | `bool` | | Do not use cache when building the image | +| `--platform` | `string` | | Set platform if server is multi-platform capable | +| `--pull` | `bool` | | Always attempt to pull a newer version of the image | +| `-q`, `--quiet` | `bool` | | Suppress the build output and print image ID on success | +| `--rm` | `bool` | `true` | Remove intermediate containers after a successful build | +| `--security-opt` | `stringSlice` | | Security options | +| `--shm-size` | `bytes` | `0` | Size of `/dev/shm` | +| `--squash` | `bool` | | Squash newly built layers into a single new layer | +| [`-t`](https://docs.docker.com/reference/cli/docker/buildx/build/#tag), [`--tag`](https://docs.docker.com/reference/cli/docker/buildx/build/#tag) | `list` | | Name and optionally a tag in the `name:tag` format | +| [`--target`](https://docs.docker.com/reference/cli/docker/buildx/build/#target) | `string` | | Set the target build stage to build. | +| `--ulimit` | `ulimit` | | Ulimit options | diff --git a/docs/reference/commandline/builder_prune.md b/docs/reference/commandline/builder_prune.md index a90d58d39a08..8dbbfbd8d220 100644 --- a/docs/reference/commandline/builder_prune.md +++ b/docs/reference/commandline/builder_prune.md @@ -7,9 +7,9 @@ Remove build cache | Name | Type | Default | Description | |:-----------------|:---------|:--------|:------------------------------------------------------| -| `-a`, `--all` | | | Remove all unused build cache, not just dangling ones | +| `-a`, `--all` | `bool` | | Remove all unused build cache, not just dangling ones | | `--filter` | `filter` | | Provide filter values (e.g. `until=24h`) | -| `-f`, `--force` | | | Do not prompt for confirmation | +| `-f`, `--force` | `bool` | | Do not prompt for confirmation | | `--keep-storage` | `bytes` | `0` | Amount of disk space to keep for cache | diff --git a/docs/reference/commandline/checkpoint_create.md b/docs/reference/commandline/checkpoint_create.md index f59e259527f5..650d5973e270 100644 --- a/docs/reference/commandline/checkpoint_create.md +++ b/docs/reference/commandline/checkpoint_create.md @@ -8,7 +8,7 @@ Create a checkpoint from a running container | Name | Type | Default | Description | |:-------------------|:---------|:--------|:---------------------------------------------| | `--checkpoint-dir` | `string` | | Use a custom checkpoint storage directory | -| `--leave-running` | | | Leave the container running after checkpoint | +| `--leave-running` | `bool` | | Leave the container running after checkpoint | diff --git a/docs/reference/commandline/cli.md b/docs/reference/commandline/cli.md deleted file mode 100644 index c6bd54337a9b..000000000000 --- a/docs/reference/commandline/cli.md +++ /dev/null @@ -1,408 +0,0 @@ ---- -title: "Use the Docker command line" -description: "Docker's CLI command description and usage" -keywords: "Docker, Docker documentation, CLI, command line, config.json, CLI configuration file" -aliases: - - /reference/commandline/cli/ - - /engine/reference/commandline/engine/ - - /engine/reference/commandline/engine_activate/ - - /engine/reference/commandline/engine_check/ - - /engine/reference/commandline/engine_update/ ---- - -The base command for the Docker CLI is `docker`. For information about the -available flags and subcommands, refer to the [CLI reference](https://docs.docker.com/reference/cli/docker/) - -Depending on your Docker system configuration, you may be required to preface -each `docker` command with `sudo`. To avoid having to use `sudo` with the -`docker` command, your system administrator can create a Unix group called -`docker` and add users to it. - -For more information about installing Docker or `sudo` configuration, refer to -the [installation](https://docs.docker.com/install/) instructions for your operating system. - -## Environment variables - -The following list of environment variables are supported by the `docker` command -line: - -| Variable | Description | -| :---------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `DOCKER_API_VERSION` | Override the negotiated API version to use for debugging (e.g. `1.19`) | -| `DOCKER_CERT_PATH` | Location of your authentication keys. This variable is used both by the `docker` CLI and the [`dockerd` daemon](https://docs.docker.com/reference/cli/dockerd/) | -| `DOCKER_CONFIG` | The location of your client configuration files. | -| `DOCKER_CONTENT_TRUST_SERVER` | The URL of the Notary server to use. Defaults to the same URL as the registry. | -| `DOCKER_CONTENT_TRUST` | When set Docker uses notary to sign and verify images. Equates to `--disable-content-trust=false` for build, create, pull, push, run. | -| `DOCKER_CONTEXT` | Name of the `docker context` to use (overrides `DOCKER_HOST` env var and default context set with `docker context use`) | -| `DOCKER_DEFAULT_PLATFORM` | Default platform for commands that take the `--platform` flag. | -| `DOCKER_HIDE_LEGACY_COMMANDS` | When set, Docker hides "legacy" top-level commands (such as `docker rm`, and `docker pull`) in `docker help` output, and only `Management commands` per object-type (e.g., `docker container`) are printed. This may become the default in a future release. | -| `DOCKER_HOST` | Daemon socket to connect to. | -| `DOCKER_TLS` | Enable TLS for connections made by the `docker` CLI (equivalent of the `--tls` command-line option). Set to a non-empty value to enable TLS. Note that TLS is enabled automatically if any of the other TLS options are set. | -| `DOCKER_TLS_VERIFY` | When set Docker uses TLS and verifies the remote. This variable is used both by the `docker` CLI and the [`dockerd` daemon](https://docs.docker.com/reference/cli/dockerd/) | -| `BUILDKIT_PROGRESS` | Set type of progress output (`auto`, `plain`, `tty`, `rawjson`) when [building](https://docs.docker.com/reference/cli/docker/image/build/) with [BuildKit backend](https://docs.docker.com/build/buildkit/). Use plain to show container output (default `auto`). | - -Because Docker is developed using Go, you can also use any environment -variables used by the Go runtime. In particular, you may find these useful: - -| Variable | Description | -|:--------------|:-------------------------------------------------------------------------------| -| `HTTP_PROXY` | Proxy URL for HTTP requests unless overridden by NoProxy. | -| `HTTPS_PROXY` | Proxy URL for HTTPS requests unless overridden by NoProxy. | -| `NO_PROXY` | Comma-separated values specifying hosts that should be excluded from proxying. | - -See the [Go specification](https://pkg.go.dev/golang.org/x/net/http/httpproxy#Config) -for details on these variables. - -## Configuration files - -By default, the Docker command line stores its configuration files in a -directory called `.docker` within your `$HOME` directory. - -Docker manages most of the files in the configuration directory -and you shouldn't modify them. However, you can modify the -`config.json` file to control certain aspects of how the `docker` -command behaves. - -You can modify the `docker` command behavior using environment -variables or command-line options. You can also use options within -`config.json` to modify some of the same behavior. If an environment variable -and the `--config` flag are set, the flag takes precedent over the environment -variable. Command line options override environment variables and environment -variables override properties you specify in a `config.json` file. - -### Change the `.docker` directory - -To specify a different directory, use the `DOCKER_CONFIG` -environment variable or the `--config` command line option. If both are -specified, then the `--config` option overrides the `DOCKER_CONFIG` environment -variable. The example below overrides the `docker ps` command using a -`config.json` file located in the `~/testconfigs/` directory. - -```console -$ docker --config ~/testconfigs/ ps -``` - -This flag only applies to whatever command is being ran. For persistent -configuration, you can set the `DOCKER_CONFIG` environment variable in your -shell (e.g. `~/.profile` or `~/.bashrc`). The example below sets the new -directory to be `HOME/newdir/.docker`. - -```console -$ echo export DOCKER_CONFIG=$HOME/newdir/.docker > ~/.profile -``` - -## Docker CLI configuration file (`config.json`) properties - - - -Use the Docker CLI configuration to customize settings for the `docker` CLI. The -configuration file uses JSON formatting, and properties: - -By default, configuration file is stored in `~/.docker/config.json`. Refer to the -[change the `.docker` directory](#change-the-docker-directory) section to use a -different location. - -> **Warning** -> -> The configuration file and other files inside the `~/.docker` configuration -> directory may contain sensitive information, such as authentication information -> for proxies or, depending on your credential store, credentials for your image -> registries. Review your configuration file's content before sharing with others, -> and prevent committing the file to version control. - -### Customize the default output format for commands - -These fields lets you customize the default output format for some commands -if no `--format` flag is provided. - -| Property | Description | -| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `configFormat` | Custom default format for `docker config ls` output. See [`docker config ls`](https://docs.docker.com/reference/cli/docker/config/ls/#format) for a list of supported formatting directives. | -| `imagesFormat` | Custom default format for `docker images` / `docker image ls` output. See [`docker images`](https://docs.docker.com/reference/cli/docker/image/ls/#format) for a list of supported formatting directives. | -| `networksFormat` | Custom default format for `docker network ls` output. See [`docker network ls`](https://docs.docker.com/reference/cli/docker/network/ls/#format) for a list of supported formatting directives. | -| `nodesFormat` | Custom default format for `docker node ls` output. See [`docker node ls`](https://docs.docker.com/reference/cli/docker/node/ls/#format) for a list of supported formatting directives. | -| `pluginsFormat` | Custom default format for `docker plugin ls` output. See [`docker plugin ls`](https://docs.docker.com/reference/cli/docker/plugin/ls/#format) for a list of supported formatting directives. | -| `psFormat` | Custom default format for `docker ps` / `docker container ps` output. See [`docker ps`](https://docs.docker.com/reference/cli/docker/container/ls/#format) for a list of supported formatting directives. | -| `secretFormat` | Custom default format for `docker secret ls` output. See [`docker secret ls`](https://docs.docker.com/reference/cli/docker/secret/ls/#format) for a list of supported formatting directives. | -| `serviceInspectFormat` | Custom default format for `docker service inspect` output. See [`docker service inspect`](https://docs.docker.com/reference/cli/docker/service/inspect/#format) for a list of supported formatting directives. | -| `servicesFormat` | Custom default format for `docker service ls` output. See [`docker service ls`](https://docs.docker.com/reference/cli/docker/service/ls/#format) for a list of supported formatting directives. | -| `statsFormat` | Custom default format for `docker stats` output. See [`docker stats`](https://docs.docker.com/reference/cli/docker/container/stats/#format) for a list of supported formatting directives. | -| `tasksFormat` | Custom default format for `docker stack ps` output. See [`docker stack ps`](https://docs.docker.com/reference/cli/docker/stack/ps/#format) for a list of supported formatting directives. | -| `volumesFormat` | Custom default format for `docker volume ls` output. See [`docker volume ls`](https://docs.docker.com/reference/cli/docker/volume/ls/#format) for a list of supported formatting directives. | - -### Custom HTTP headers - -The property `HttpHeaders` specifies a set of headers to include in all messages -sent from the Docker client to the daemon. Docker doesn't try to interpret or -understand these headers; it simply puts them into the messages. Docker does -not allow these headers to change any headers it sets for itself. - -### Credential store options - -The property `credsStore` specifies an external binary to serve as the default -credential store. When this property is set, `docker login` will attempt to -store credentials in the binary specified by `docker-credential-` which -is visible on `$PATH`. If this property isn't set, credentials are stored -in the `auths` property of the CLI configuration file. For more information, -see the [**Credential stores** section in the `docker login` documentation](https://docs.docker.com/reference/cli/docker/login/#credential-stores) - -The property `credHelpers` specifies a set of credential helpers to use -preferentially over `credsStore` or `auths` when storing and retrieving -credentials for specific registries. If this property is set, the binary -`docker-credential-` will be used when storing or retrieving credentials -for a specific registry. For more information, see the -[**Credential helpers** section in the `docker login` documentation](https://docs.docker.com/reference/cli/docker/login/#credential-helpers) - -### Automatic proxy configuration for containers - -The property `proxies` specifies proxy environment variables to be automatically -set on containers, and set as `--build-arg` on containers used during `docker build`. -A `"default"` set of proxies can be configured, and will be used for any Docker -daemon that the client connects to, or a configuration per host (Docker daemon), -for example, `https://docker-daemon1.example.com`. The following properties can -be set for each environment: - -| Property | Description | -|:---------------|:--------------------------------------------------------------------------------------------------------| -| `httpProxy` | Default value of `HTTP_PROXY` and `http_proxy` for containers, and as `--build-arg` on `docker build` | -| `httpsProxy` | Default value of `HTTPS_PROXY` and `https_proxy` for containers, and as `--build-arg` on `docker build` | -| `ftpProxy` | Default value of `FTP_PROXY` and `ftp_proxy` for containers, and as `--build-arg` on `docker build` | -| `noProxy` | Default value of `NO_PROXY` and `no_proxy` for containers, and as `--build-arg` on `docker build` | -| `allProxy` | Default value of `ALL_PROXY` and `all_proxy` for containers, and as `--build-arg` on `docker build` | - -These settings are used to configure proxy settings for containers only, and not -used as proxy settings for the `docker` CLI or the `dockerd` daemon. Refer to the -[environment variables](#environment-variables) and [HTTP/HTTPS proxy](https://docs.docker.com/config/daemon/systemd/#httphttps-proxy) -sections for configuring proxy settings for the cli and daemon. - -> **Warning** -> -> Proxy settings may contain sensitive information (for example, if the proxy -> requires authentication). Environment variables are stored as plain text in -> the container's configuration, and as such can be inspected through the remote -> API or committed to an image when using `docker commit`. -{ .warning } - -### Default key-sequence to detach from containers - -Once attached to a container, users detach from it and leave it running using -the using `CTRL-p CTRL-q` key sequence. This detach key sequence is customizable -using the `detachKeys` property. Specify a `` value for the -property. The format of the `` is a comma-separated list of either -a letter [a-Z], or the `ctrl-` combined with any of the following: - -* `a-z` (a single lowercase alpha character ) -* `@` (at sign) -* `[` (left bracket) -* `\\` (two backward slashes) -* `_` (underscore) -* `^` (caret) - -Your customization applies to all containers started in with your Docker client. -Users can override your custom or the default key sequence on a per-container -basis. To do this, the user specifies the `--detach-keys` flag with the `docker -attach`, `docker exec`, `docker run` or `docker start` command. - -### CLI plugin options - -The property `plugins` contains settings specific to CLI plugins. The -key is the plugin name, while the value is a further map of options, -which are specific to that plugin. - -### Sample configuration file - -Following is a sample `config.json` file to illustrate the format used for -various fields: - -```json -{ - "HttpHeaders": { - "MyHeader": "MyValue" - }, - "psFormat": "table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}", - "imagesFormat": "table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}", - "pluginsFormat": "table {{.ID}}\t{{.Name}}\t{{.Enabled}}", - "statsFormat": "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}", - "servicesFormat": "table {{.ID}}\t{{.Name}}\t{{.Mode}}", - "secretFormat": "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}", - "configFormat": "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}", - "serviceInspectFormat": "pretty", - "nodesFormat": "table {{.ID}}\t{{.Hostname}}\t{{.Availability}}", - "detachKeys": "ctrl-e,e", - "credsStore": "secretservice", - "credHelpers": { - "awesomereg.example.org": "hip-star", - "unicorn.example.com": "vcbait" - }, - "plugins": { - "plugin1": { - "option": "value" - }, - "plugin2": { - "anotheroption": "anothervalue", - "athirdoption": "athirdvalue" - } - }, - "proxies": { - "default": { - "httpProxy": "http://user:pass@example.com:3128", - "httpsProxy": "https://my-proxy.example.com:3129", - "noProxy": "intra.mycorp.example.com", - "ftpProxy": "http://user:pass@example.com:3128", - "allProxy": "socks://example.com:1234" - }, - "https://manager1.mycorp.example.com:2377": { - "httpProxy": "http://user:pass@example.com:3128", - "httpsProxy": "https://my-proxy.example.com:3129" - } - } -} -``` - -### Experimental features - -Experimental features provide early access to future product functionality. -These features are intended for testing and feedback, and they may change -between releases without warning or can be removed from a future release. - -Starting with Docker 20.10, experimental CLI features are enabled by default, -and require no configuration to enable them. - -### Notary - -If using your own notary server and a self-signed certificate or an internal -Certificate Authority, you need to place the certificate at -`tls//ca.crt` in your Docker config directory. - -Alternatively you can trust the certificate globally by adding it to your system's -list of root Certificate Authorities. - -## Examples - -### Specify daemon host (-H, --host) - -You can use the `-H`, `--host` flag to specify a socket to use when you invoke -a `docker` command. You can use the following protocols: - -| Scheme | Description | Example | -|----------------------------------------|---------------------------|----------------------------------| -| `unix://[]` | Unix socket (Linux only) | `unix:///var/run/docker.sock` | -| `tcp://[[:port]]` | TCP connection | `tcp://174.17.0.1:2376` | -| `ssh://[username@][:port]` | SSH connection | `ssh://user@192.168.64.5` | -| `npipe://[]` | Named pipe (Windows only) | `npipe:////./pipe/docker_engine` | - -If you don't specify the `-H` flag, and you're not using a custom -[context](https://docs.docker.com/engine/context/working-with-contexts), -commands use the following default sockets: - -- `unix:///var/run/docker.sock` on macOS and Linux -- `npipe:////./pipe/docker_engine` on Windows - -To achieve a similar effect without having to specify the `-H` flag for every -command, you could also [create a context](https://docs.docker.com/reference/cli/docker/context/create/), -or alternatively, use the -[`DOCKER_HOST` environment variable](#environment-variables). - -For more information about the `-H` flag, see -[Daemon socket option](https://docs.docker.com/reference/cli/dockerd/#daemon-socket-option). - -#### Using TCP sockets - -The following example shows how to invoke `docker ps` over TCP, to a remote -daemon with IP address `174.17.0.1`, listening on port `2376`: - -```console -$ docker -H tcp://174.17.0.1:2376 ps -``` - -> **Note** -> -> By convention, the Docker daemon uses port `2376` for secure TLS connections, -> and port `2375` for insecure, non-TLS connections. - -#### Using SSH sockets - -When you use SSH invoke a command on a remote daemon, the request gets forwarded -to the `/var/run/docker.sock` Unix socket on the SSH host. - -```console -$ docker -H ssh://user@192.168.64.5 ps -``` - -You can optionally specify the location of the socket by appending a path -component to the end of the SSH address. - -```console -$ docker -H ssh://user@192.168.64.5/var/run/docker.sock ps -``` - -### Display help text - -To list the help on any command just execute the command, followed by the -`--help` option. - -```console -$ docker run --help - -Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - -Create and run a new container from an image - -Options: - --add-host value Add a custom host-to-IP mapping (host:ip) (default []) - -a, --attach value Attach to STDIN, STDOUT or STDERR (default []) -<...> -``` - -### Option types - -Single character command line options can be combined, so rather than -typing `docker run -i -t --name test busybox sh`, -you can write `docker run -it --name test busybox sh`. - -#### Boolean - -Boolean options take the form `-d=false`. The value you see in the help text is -the default value which is set if you do **not** specify that flag. If you -specify a Boolean flag without a value, this will set the flag to `true`, -irrespective of the default value. - -For example, running `docker run -d` will set the value to `true`, so your -container **will** run in "detached" mode, in the background. - -Options which default to `true` (e.g., `docker build --rm=true`) can only be -set to the non-default value by explicitly setting them to `false`: - -```console -$ docker build --rm=false . -``` - -#### Multi - -You can specify options like `-a=[]` multiple times in a single command line, -for example in these commands: - -```console -$ docker run -a stdin -a stdout -i -t ubuntu /bin/bash - -$ docker run -a stdin -a stdout -a stderr ubuntu /bin/ls -``` - -Sometimes, multiple options can call for a more complex value string as for -`-v`: - -```console -$ docker run -v /host:/container example/mysql -``` - -> **Note** -> -> Do not use the `-t` and `-a stderr` options together due to -> limitations in the `pty` implementation. All `stderr` in `pty` mode -> simply goes to `stdout`. - -#### Strings and Integers - -Options like `--name=""` expect a string, and they -can only be specified once. Options like `-c=0` -expect an integer, and they can only be specified once. diff --git a/docs/reference/commandline/config_create.md b/docs/reference/commandline/config_create.md index 55ffd3bdf1f5..f9ed5fc438e9 100644 --- a/docs/reference/commandline/config_create.md +++ b/docs/reference/commandline/config_create.md @@ -19,8 +19,7 @@ Creates a config using standard input or from a file for the config content. For detailed information about using configs, refer to [store configuration data using Docker Configs](https://docs.docker.com/engine/swarm/configs/). -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a Swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/config_inspect.md b/docs/reference/commandline/config_inspect.md index 5758d3dbec16..7893a843692f 100644 --- a/docs/reference/commandline/config_inspect.md +++ b/docs/reference/commandline/config_inspect.md @@ -8,7 +8,7 @@ Display detailed information on one or more configs | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`-f`](#format), [`--format`](#format) | `string` | | Format output using a custom template:
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `--pretty` | | | Print the information in a human friendly format | +| `--pretty` | `bool` | | Print the information in a human friendly format | @@ -25,8 +25,7 @@ describes all the details of the format. For detailed information about using configs, refer to [store configuration data using Docker Configs](https://docs.docker.com/engine/swarm/configs/). -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a Swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/config_ls.md b/docs/reference/commandline/config_ls.md index be9ddb74116a..1f5ad9e844ab 100644 --- a/docs/reference/commandline/config_ls.md +++ b/docs/reference/commandline/config_ls.md @@ -13,7 +13,7 @@ List configs |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `-q`, `--quiet` | | | Only display IDs | +| `-q`, `--quiet` | `bool` | | Only display IDs | @@ -24,8 +24,7 @@ Run this command on a manager node to list the configs in the Swarm. For detailed information about using configs, refer to [store configuration data using Docker Configs](https://docs.docker.com/engine/swarm/configs/). -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a Swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/config_rm.md b/docs/reference/commandline/config_rm.md index 455e7ba7c992..d7a1a3f78716 100644 --- a/docs/reference/commandline/config_rm.md +++ b/docs/reference/commandline/config_rm.md @@ -16,8 +16,7 @@ Removes the specified configs from the Swarm. For detailed information about using configs, refer to [store configuration data using Docker Configs](https://docs.docker.com/engine/swarm/configs/). -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a Swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the @@ -32,8 +31,7 @@ $ docker config rm my_config sapth4csdo5b6wz2p5uimh5xg ``` -> **Warning** -> +> [!WARNING] > This command doesn't ask for confirmation before removing a config. { .warning } diff --git a/docs/reference/commandline/container_attach.md b/docs/reference/commandline/container_attach.md index beb6ab4c80f9..9947c64f2094 100644 --- a/docs/reference/commandline/container_attach.md +++ b/docs/reference/commandline/container_attach.md @@ -12,7 +12,7 @@ Attach local standard input, output, and error streams to a running container | Name | Type | Default | Description | |:--------------------------------|:---------|:--------|:----------------------------------------------------| | [`--detach-keys`](#detach-keys) | `string` | | Override the key sequence for detaching a container | -| `--no-stdin` | | | Do not attach STDIN | +| `--no-stdin` | `bool` | | Do not attach STDIN | | `--sig-proxy` | `bool` | `true` | Proxy all received signals to the process | @@ -25,8 +25,7 @@ Use `docker attach` to attach your terminal's standard input, output, and error ID or name. This lets you view its output or control it interactively, as though the commands were running directly in your terminal. -> **Note** -> +> [!NOTE] > The `attach` command displays the output of the container's `ENTRYPOINT` and > `CMD` process. This can appear as if the attach command is hung when in fact > the process may simply not be writing any output at that time. @@ -39,8 +38,7 @@ container. If `--sig-proxy` is true (the default),`CTRL-c` sends a `SIGINT` to the container. If the container was run with `-i` and `-t`, you can detach from a container and leave it running using the `CTRL-p CTRL-q` key sequence. -> **Note** -> +> [!NOTE] > A process running as PID 1 inside a container is treated specially by > Linux: it ignores any signal with the default action. So, the process > doesn't terminate on `SIGINT` or `SIGTERM` unless it's coded to do so. @@ -97,7 +95,7 @@ CONTAINER ID IMAGE COMMAND CREATED STATUS Repeating the example above, but this time with the `-i` and `-t` options set; ```console -$ docker run -dit --name topdemo2 ubuntu:22.04 /usr/bin/top -b +$ docker run -dit --name topdemo2 alpine /usr/bin/top -b ``` Now, when attaching to the container, and pressing the `CTRL-p CTRL-q` ("read @@ -164,4 +162,4 @@ the following: These `a`, `ctrl-a`, `X`, or `ctrl-\\` values are all examples of valid key sequences. To configure a different configuration default key sequence for all -containers, see [**Configuration file** section](https://docs.docker.com/engine/reference/commandline/cli/#configuration-files). +containers, see [**Configuration file** section](https://docs.docker.com/reference/cli/docker/#configuration-files). diff --git a/docs/reference/commandline/container_commit.md b/docs/reference/commandline/container_commit.md index c7bad61ee98c..636e85ba29bd 100644 --- a/docs/reference/commandline/container_commit.md +++ b/docs/reference/commandline/container_commit.md @@ -44,8 +44,8 @@ created. Supported `Dockerfile` instructions: $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -c3f279d17e0a ubuntu:22.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky -197387f1b436 ubuntu:22.04 /bin/bash 7 days ago Up 25 hours focused_hamilton +c3f279d17e0a ubuntu:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky +197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton $ docker commit c3f279d17e0a svendowideit/testimage:version3 @@ -63,8 +63,8 @@ svendowideit/testimage version3 f5283438590d 16 sec $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -c3f279d17e0a ubuntu:22.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky -197387f1b436 ubuntu:22.04 /bin/bash 7 days ago Up 25 hours focused_hamilton +c3f279d17e0a ubuntu:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky +197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton $ docker inspect -f "{{ .Config.Env }}" c3f279d17e0a @@ -85,8 +85,8 @@ $ docker inspect -f "{{ .Config.Env }}" f5283438590d $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -c3f279d17e0a ubuntu:22.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky -197387f1b436 ubuntu:22.04 /bin/bash 7 days ago Up 25 hours focused_hamilton +c3f279d17e0a ubuntu:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky +197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton $ docker commit --change='CMD ["apachectl", "-DFOREGROUND"]' -c "EXPOSE 80" c3f279d17e0a svendowideit/testimage:version4 @@ -100,6 +100,6 @@ $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 89373736e2e7 testimage:version4 "apachectl -DFOREGROU" 3 seconds ago Up 2 seconds 80/tcp distracted_fermat -c3f279d17e0a ubuntu:22.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky -197387f1b436 ubuntu:22.04 /bin/bash 7 days ago Up 25 hours focused_hamilton +c3f279d17e0a ubuntu:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky +197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton ``` diff --git a/docs/reference/commandline/container_cp.md b/docs/reference/commandline/container_cp.md index 3aebacabdbf3..9a6166625988 100644 --- a/docs/reference/commandline/container_cp.md +++ b/docs/reference/commandline/container_cp.md @@ -14,11 +14,11 @@ container source to stdout. ### Options -| Name | Type | Default | Description | -|:----------------------|:-----|:--------|:-------------------------------------------------------------------------------------------------------------| -| `-a`, `--archive` | | | Archive mode (copy all uid/gid information) | -| `-L`, `--follow-link` | | | Always follow symbol link in SRC_PATH | -| `-q`, `--quiet` | | | Suppress progress output during copy. Progress output is automatically suppressed if no terminal is attached | +| Name | Type | Default | Description | +|:----------------------|:-------|:--------|:-------------------------------------------------------------------------------------------------------------| +| `-a`, `--archive` | `bool` | | Archive mode (copy all uid/gid information) | +| `-L`, `--follow-link` | `bool` | | Always follow symbol link in SRC_PATH | +| `-q`, `--quiet` | `bool` | | Suppress progress output during copy. Progress output is automatically suppressed if no terminal is attached | diff --git a/docs/reference/commandline/container_create.md b/docs/reference/commandline/container_create.md index dc76075ad389..a2394eab28c2 100644 --- a/docs/reference/commandline/container_create.md +++ b/docs/reference/commandline/container_create.md @@ -54,10 +54,10 @@ Create a new container | `--health-start-interval` | `duration` | `0s` | Time between running the check during the start period (ms\|s\|m\|h) (default 0s) | | `--health-start-period` | `duration` | `0s` | Start period for the container to initialize before starting health-retries countdown (ms\|s\|m\|h) (default 0s) | | `--health-timeout` | `duration` | `0s` | Maximum time to allow one check to run (ms\|s\|m\|h) (default 0s) | -| `--help` | | | Print usage | +| `--help` | `bool` | | Print usage | | `-h`, `--hostname` | `string` | | Container host name | -| `--init` | | | Run an init inside the container that forwards signals and reaps processes | -| `-i`, `--interactive` | | | Keep STDIN open even if not attached | +| `--init` | `bool` | | Run an init inside the container that forwards signals and reaps processes | +| `-i`, `--interactive` | `bool` | | Keep STDIN open even if not attached | | `--io-maxbandwidth` | `bytes` | `0` | Maximum IO bandwidth limit for the system drive (Windows only) | | `--io-maxiops` | `uint64` | `0` | Maximum IOps limit for the system drive (Windows only) | | `--ip` | `string` | | IPv4 address (e.g., 172.30.100.104) | @@ -80,20 +80,20 @@ Create a new container | `--name` | `string` | | Assign a name to the container | | `--network` | `network` | | Connect a container to a network | | `--network-alias` | `list` | | Add network-scoped alias for the container | -| `--no-healthcheck` | | | Disable any container-specified HEALTHCHECK | -| `--oom-kill-disable` | | | Disable OOM Killer | +| `--no-healthcheck` | `bool` | | Disable any container-specified HEALTHCHECK | +| `--oom-kill-disable` | `bool` | | Disable OOM Killer | | `--oom-score-adj` | `int` | `0` | Tune host's OOM preferences (-1000 to 1000) | | `--pid` | `string` | | PID namespace to use | | `--pids-limit` | `int64` | `0` | Tune container pids limit (set -1 for unlimited) | | `--platform` | `string` | | Set platform if server is multi-platform capable | -| `--privileged` | | | Give extended privileges to this container | +| `--privileged` | `bool` | | Give extended privileges to this container | | `-p`, `--publish` | `list` | | Publish a container's port(s) to the host | -| `-P`, `--publish-all` | | | Publish all exposed ports to random ports | +| `-P`, `--publish-all` | `bool` | | Publish all exposed ports to random ports | | `--pull` | `string` | `missing` | Pull image before creating (`always`, `\|missing`, `never`) | -| `-q`, `--quiet` | | | Suppress the pull output | -| `--read-only` | | | Mount the container's root filesystem as read only | +| `-q`, `--quiet` | `bool` | | Suppress the pull output | +| `--read-only` | `bool` | | Mount the container's root filesystem as read only | | `--restart` | `string` | `no` | Restart policy to apply when a container exits | -| `--rm` | | | Automatically remove the container and its associated anonymous volumes when it exits | +| `--rm` | `bool` | | Automatically remove the container and its associated anonymous volumes when it exits | | `--runtime` | `string` | | Runtime to use for this container | | `--security-opt` | `list` | | Security Options | | `--shm-size` | `bytes` | `0` | Size of /dev/shm | @@ -102,7 +102,7 @@ Create a new container | `--storage-opt` | `list` | | Storage driver options for the container | | `--sysctl` | `map` | `map[]` | Sysctl options | | `--tmpfs` | `list` | | Mount a tmpfs directory | -| `-t`, `--tty` | | | Allocate a pseudo-TTY | +| `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY | | `--ulimit` | `ulimit` | | Ulimit options | | `-u`, `--user` | `string` | | Username or UID (format: [:]) | | `--userns` | `string` | | User namespace to use | diff --git a/docs/reference/commandline/container_exec.md b/docs/reference/commandline/container_exec.md index 896c05ae6c40..7aeea01b6edf 100644 --- a/docs/reference/commandline/container_exec.md +++ b/docs/reference/commandline/container_exec.md @@ -11,13 +11,13 @@ Execute a command in a running container | Name | Type | Default | Description | |:------------------------------------------|:---------|:--------|:-------------------------------------------------------| -| `-d`, `--detach` | | | Detached mode: run command in the background | +| `-d`, `--detach` | `bool` | | Detached mode: run command in the background | | `--detach-keys` | `string` | | Override the key sequence for detaching a container | | [`-e`](#env), [`--env`](#env) | `list` | | Set environment variables | | `--env-file` | `list` | | Read in a file of environment variables | -| `-i`, `--interactive` | | | Keep STDIN open even if not attached | -| [`--privileged`](#privileged) | | | Give extended privileges to the command | -| `-t`, `--tty` | | | Allocate a pseudo-TTY | +| `-i`, `--interactive` | `bool` | | Keep STDIN open even if not attached | +| [`--privileged`](#privileged) | `bool` | | Give extended privileges to the command | +| `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY | | `-u`, `--user` | `string` | | Username or UID (format: `[:]`) | | [`-w`](#workdir), [`--workdir`](#workdir) | `string` | | Working directory inside the container | diff --git a/docs/reference/commandline/container_export.md b/docs/reference/commandline/container_export.md index 31fd70a774fb..d974991f1f2f 100644 --- a/docs/reference/commandline/container_export.md +++ b/docs/reference/commandline/container_export.md @@ -23,7 +23,7 @@ with the container. If a volume is mounted on top of an existing directory in the container, `docker export` exports the contents of the underlying directory, not the contents of the volume. -Refer to [Backup, restore, or migrate data volumes](https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes) +Refer to [Backup, restore, or migrate data volumes](https://docs.docker.com/engine/storage/volumes/#back-up-restore-or-migrate-data-volumes) in the user guide for examples on exporting data in a volume. ## Examples diff --git a/docs/reference/commandline/container_inspect.md b/docs/reference/commandline/container_inspect.md index 8ebd2157aede..0ca805ded4a6 100644 --- a/docs/reference/commandline/container_inspect.md +++ b/docs/reference/commandline/container_inspect.md @@ -8,7 +8,7 @@ Display detailed information on one or more containers | Name | Type | Default | Description | |:-----------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `-f`, `--format` | `string` | | Format output using a custom template:
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `-s`, `--size` | | | Display total file sizes | +| `-s`, `--size` | `bool` | | Display total file sizes | diff --git a/docs/reference/commandline/container_kill.md b/docs/reference/commandline/container_kill.md index 2d12d6eb4d45..394ce043103c 100644 --- a/docs/reference/commandline/container_kill.md +++ b/docs/reference/commandline/container_kill.md @@ -33,8 +33,7 @@ set through `--signal` may be non-terminal, depending on the container's main process. For example, the `SIGHUP` signal in most cases will be non-terminal, and the container will continue running after receiving the signal. -> **Note** -> +> [!NOTE] > `ENTRYPOINT` and `CMD` in the *shell* form run as a child process of > `/bin/sh -c`, which does not pass signals. This means that the executable is > not the container’s PID 1 and does not receive Unix signals. diff --git a/docs/reference/commandline/container_logs.md b/docs/reference/commandline/container_logs.md index 9b4d29cdd170..4fca9817c590 100644 --- a/docs/reference/commandline/container_logs.md +++ b/docs/reference/commandline/container_logs.md @@ -11,11 +11,11 @@ Fetch the logs of a container | Name | Type | Default | Description | |:---------------------|:---------|:--------|:---------------------------------------------------------------------------------------------------| -| `--details` | | | Show extra details provided to logs | -| `-f`, `--follow` | | | Follow log output | +| `--details` | `bool` | | Show extra details provided to logs | +| `-f`, `--follow` | `bool` | | Follow log output | | `--since` | `string` | | Show logs since timestamp (e.g. `2013-01-02T13:23:37Z`) or relative (e.g. `42m` for 42 minutes) | | `-n`, `--tail` | `string` | `all` | Number of lines to show from the end of the logs | -| `-t`, `--timestamps` | | | Show timestamps | +| `-t`, `--timestamps` | `bool` | | Show timestamps | | [`--until`](#until) | `string` | | Show logs before a timestamp (e.g. `2013-01-02T13:23:37Z`) or relative (e.g. `42m` for 42 minutes) | @@ -26,7 +26,7 @@ Fetch the logs of a container The `docker logs` command batch-retrieves logs present at the time of execution. For more information about selecting and configuring logging drivers, refer to -[Configure logging drivers](https://docs.docker.com/config/containers/logging/configure/). +[Configure logging drivers](https://docs.docker.com/engine/logging/configure/). The `docker logs --follow` command will continue streaming the new output from the container's `STDOUT` and `STDERR`. diff --git a/docs/reference/commandline/container_ls.md b/docs/reference/commandline/container_ls.md index 062fdcb4cd1b..a19f7a5e0410 100644 --- a/docs/reference/commandline/container_ls.md +++ b/docs/reference/commandline/container_ls.md @@ -11,14 +11,14 @@ List containers | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [`-a`](#all), [`--all`](#all) | | | Show all containers (default shows just running) | +| [`-a`](#all), [`--all`](#all) | `bool` | | Show all containers (default shows just running) | | [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `-n`, `--last` | `int` | `-1` | Show n last created containers (includes all states) | -| `-l`, `--latest` | | | Show the latest created container (includes all states) | -| [`--no-trunc`](#no-trunc) | | | Don't truncate output | -| `-q`, `--quiet` | | | Only display container IDs | -| [`-s`](#size), [`--size`](#size) | | | Display total file sizes | +| `-l`, `--latest` | `bool` | | Show the latest created container (includes all states) | +| [`--no-trunc`](#no-trunc) | `bool` | | Don't truncate output | +| `-q`, `--quiet` | `bool` | | Only display container IDs | +| [`-s`](#size), [`--size`](#size) | `bool` | | Display total file sizes | @@ -33,7 +33,7 @@ Running `docker ps --no-trunc` showing 2 linked containers. $ docker ps --no-trunc CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -ca5534a51dd04bbcebe9b23ba05f389466cf0c190f1f8f182d7eea92a9671d00 ubuntu:22.04 bash 17 seconds ago Up 16 seconds 3300-3310/tcp webapp +ca5534a51dd04bbcebe9b23ba05f389466cf0c190f1f8f182d7eea92a9671d00 ubuntu:24.04 bash 17 seconds ago Up 16 seconds 3300-3310/tcp webapp 9ca9747b233100676a48cc7806131586213fa5dab86dd1972d6a8732e3a84a4d crosbymichael/redis:latest /redis-server --dir 33 minutes ago Up 33 minutes 6379/tcp redis,webapp/db ``` @@ -64,7 +64,7 @@ e90b8831a4b8 nginx "/bin/bash -c 'mkdir " 11 weeks ago Up 4 hours * The "size" information shows the amount of data (on disk) that is used for the _writable_ layer of each container * The "virtual size" is the total amount of disk-space used for the read-only _image_ data used by the container and the writable layer. -For more information, refer to the [container size on disk](https://docs.docker.com/storage/storagedriver/#container-size-on-disk) section. +For more information, refer to the [container size on disk](https://docs.docker.com/engine/storage/drivers/#container-size-on-disk) section. ### Filtering (--filter) @@ -240,13 +240,13 @@ CONTAINER ID IMAGE COMMAND CREATED 919e1179bdb8 ubuntu-c1 "top" About a minute ago Up About a minute admiring_lovelace ``` -Match containers based on the `ubuntu` version `22.04` image: +Match containers based on the `ubuntu` version `24.04` image: ```console -$ docker ps --filter ancestor=ubuntu:22.04 +$ docker ps --filter ancestor=ubuntu:24.04 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -82a598284012 ubuntu:22.04 "top" 3 minutes ago Up 3 minutes sleepy_bose +82a598284012 ubuntu:24.04 "top" 3 minutes ago Up 3 minutes sleepy_bose ``` The following matches containers based on the layer `d0e008c6cf02` or an image @@ -256,7 +256,7 @@ that have this layer in its layer stack. $ docker ps --filter ancestor=d0e008c6cf02 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -82a598284012 ubuntu:22.04 "top" 3 minutes ago Up 3 minutes sleepy_bose +82a598284012 ubuntu:24.04 "top" 3 minutes ago Up 3 minutes sleepy_bose ``` #### Create time diff --git a/docs/reference/commandline/container_prune.md b/docs/reference/commandline/container_prune.md index 8450625ddcee..dd753aa26b8d 100644 --- a/docs/reference/commandline/container_prune.md +++ b/docs/reference/commandline/container_prune.md @@ -8,7 +8,7 @@ Remove all stopped containers | Name | Type | Default | Description | |:----------------------|:---------|:--------|:-------------------------------------------------| | [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `until=`) | -| `-f`, `--force` | | | Do not prompt for confirmation | +| `-f`, `--force` | `bool` | | Do not prompt for confirmation | @@ -43,7 +43,7 @@ The currently supported filters are: * label (`label=`, `label==`, `label!=`, or `label!==`) - only remove containers with (or without, in case `label!=...` is used) the specified labels. The `until` filter can be Unix timestamps, date formatted -timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed +timestamps, or Go duration strings supported by [ParseDuration](https://pkg.go.dev/time#ParseDuration) (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. Supported formats for date formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`, `2006-01-02T15:04:05.999999999`, `2006-01-02T07:00`, and `2006-01-02`. The local diff --git a/docs/reference/commandline/container_restart.md b/docs/reference/commandline/container_restart.md index 7eb5b033dcfa..804fa072bfba 100644 --- a/docs/reference/commandline/container_restart.md +++ b/docs/reference/commandline/container_restart.md @@ -9,10 +9,10 @@ Restart one or more containers ### Options -| Name | Type | Default | Description | -|:-----------------|:---------|:--------|:---------------------------------------------| -| `-s`, `--signal` | `string` | | Signal to send to the container | -| `-t`, `--time` | `int` | `0` | Seconds to wait before killing the container | +| Name | Type | Default | Description | +|:---------------------------------------|:---------|:--------|:---------------------------------------------| +| [`-s`](#signal), [`--signal`](#signal) | `string` | | Signal to send to the container | +| [`-t`](#time), [`--time`](#time) | `int` | `0` | Seconds to wait before killing the container | @@ -22,3 +22,34 @@ Restart one or more containers ```console $ docker restart my_container ``` + + +### Stop container with signal (-s, --signal) + +The `--signal` flag sends the system call signal to the container to exit. +This signal can be a signal name in the format `SIG`, for instance +`SIGKILL`, or an unsigned number that matches a position in the kernel's +syscall table, for instance `9`. Refer to [signal(7)](https://man7.org/linux/man-pages/man7/signal.7.html) +for available signals. + +The default signal to use is defined by the image's [`StopSignal`](https://github.com/opencontainers/image-spec/blob/v1.1.0/config.md), +which can be set through the [`STOPSIGNAL`](https://docs.docker.com/reference/dockerfile/#stopsignal) +Dockerfile instruction when building the image, or configured using the +[`--stop-signal`](https://docs.docker.com/reference/cli/docker/container/run/#stop-signal) +option when creating the container. If no signal is configured for the +container, `SIGTERM` is used as default. + +### Stop container with timeout (-t, --timeout) + +The `--time` flag sets the number of seconds to wait for the container +to stop after sending the pre-defined (see [`--signal`]{#signal)) system call signal. +If the container does not exit after the timeout elapses, it's forcibly killed +with a `SIGKILL` signal. + +If you set `--time` to `-1`, no timeout is applied, and the daemon +waits indefinitely for the container to exit. + +The default timeout can be specified using the [`--stop-timeout`](https://docs.docker.com/reference/cli/docker/container/run/#stop-timeout) +option when creating the container. If no default is configured for the container, +the Daemon determines the default, and is 10 seconds for Linux containers, and +30 seconds for Windows containers. diff --git a/docs/reference/commandline/container_rm.md b/docs/reference/commandline/container_rm.md index cbb8b81c4558..ee370f71e43b 100644 --- a/docs/reference/commandline/container_rm.md +++ b/docs/reference/commandline/container_rm.md @@ -9,11 +9,11 @@ Remove one or more containers ### Options -| Name | Type | Default | Description | -|:------------------------------------------|:-----|:--------|:--------------------------------------------------------| -| [`-f`](#force), [`--force`](#force) | | | Force the removal of a running container (uses SIGKILL) | -| [`-l`](#link), [`--link`](#link) | | | Remove the specified link | -| [`-v`](#volumes), [`--volumes`](#volumes) | | | Remove anonymous volumes associated with the container | +| Name | Type | Default | Description | +|:------------------------------------------|:-------|:--------|:--------------------------------------------------------| +| [`-f`](#force), [`--force`](#force) | `bool` | | Force the removal of a running container (uses SIGKILL) | +| [`-l`](#link), [`--link`](#link) | `bool` | | Remove the specified link | +| [`-v`](#volumes), [`--volumes`](#volumes) | `bool` | | Remove anonymous volumes associated with the container | diff --git a/docs/reference/commandline/container_run.md b/docs/reference/commandline/container_run.md index 79b557d81d96..98e86e907d8e 100644 --- a/docs/reference/commandline/container_run.md +++ b/docs/reference/commandline/container_run.md @@ -31,7 +31,7 @@ Create and run a new container from an image | `--cpus` | `decimal` | | Number of CPUs | | `--cpuset-cpus` | `string` | | CPUs in which to allow execution (0-3, 0,1) | | `--cpuset-mems` | `string` | | MEMs in which to allow execution (0-3, 0,1) | -| [`-d`](#detach), [`--detach`](#detach) | | | Run container in background and print container ID | +| [`-d`](#detach), [`--detach`](#detach) | `bool` | | Run container in background and print container ID | | [`--detach-keys`](#detach-keys) | `string` | | Override the key sequence for detaching a container | | [`--device`](#device) | `list` | | Add a host device to the container | | [`--device-cgroup-rule`](#device-cgroup-rule) | `list` | | Add a rule to the cgroup allowed devices list | @@ -56,10 +56,10 @@ Create and run a new container from an image | `--health-start-interval` | `duration` | `0s` | Time between running the check during the start period (ms\|s\|m\|h) (default 0s) | | `--health-start-period` | `duration` | `0s` | Start period for the container to initialize before starting health-retries countdown (ms\|s\|m\|h) (default 0s) | | `--health-timeout` | `duration` | `0s` | Maximum time to allow one check to run (ms\|s\|m\|h) (default 0s) | -| `--help` | | | Print usage | +| `--help` | `bool` | | Print usage | | `-h`, `--hostname` | `string` | | Container host name | -| [`--init`](#init) | | | Run an init inside the container that forwards signals and reaps processes | -| [`-i`](#interactive), [`--interactive`](#interactive) | | | Keep STDIN open even if not attached | +| [`--init`](#init) | `bool` | | Run an init inside the container that forwards signals and reaps processes | +| [`-i`](#interactive), [`--interactive`](#interactive) | `bool` | | Keep STDIN open even if not attached | | `--io-maxbandwidth` | `bytes` | `0` | Maximum IO bandwidth limit for the system drive (Windows only) | | `--io-maxiops` | `uint64` | `0` | Maximum IOps limit for the system drive (Windows only) | | `--ip` | `string` | | IPv4 address (e.g., 172.30.100.104) | @@ -82,20 +82,20 @@ Create and run a new container from an image | [`--name`](#name) | `string` | | Assign a name to the container | | [`--network`](#network) | `network` | | Connect a container to a network | | `--network-alias` | `list` | | Add network-scoped alias for the container | -| `--no-healthcheck` | | | Disable any container-specified HEALTHCHECK | -| `--oom-kill-disable` | | | Disable OOM Killer | +| `--no-healthcheck` | `bool` | | Disable any container-specified HEALTHCHECK | +| `--oom-kill-disable` | `bool` | | Disable OOM Killer | | `--oom-score-adj` | `int` | `0` | Tune host's OOM preferences (-1000 to 1000) | | [`--pid`](#pid) | `string` | | PID namespace to use | | `--pids-limit` | `int64` | `0` | Tune container pids limit (set -1 for unlimited) | | `--platform` | `string` | | Set platform if server is multi-platform capable | -| [`--privileged`](#privileged) | | | Give extended privileges to this container | +| [`--privileged`](#privileged) | `bool` | | Give extended privileges to this container | | [`-p`](#publish), [`--publish`](#publish) | `list` | | Publish a container's port(s) to the host | -| [`-P`](#publish-all), [`--publish-all`](#publish-all) | | | Publish all exposed ports to random ports | +| [`-P`](#publish-all), [`--publish-all`](#publish-all) | `bool` | | Publish all exposed ports to random ports | | [`--pull`](#pull) | `string` | `missing` | Pull image before running (`always`, `missing`, `never`) | -| `-q`, `--quiet` | | | Suppress the pull output | -| [`--read-only`](#read-only) | | | Mount the container's root filesystem as read only | +| `-q`, `--quiet` | `bool` | | Suppress the pull output | +| [`--read-only`](#read-only) | `bool` | | Mount the container's root filesystem as read only | | [`--restart`](#restart) | `string` | `no` | Restart policy to apply when a container exits | -| [`--rm`](#rm) | | | Automatically remove the container and its associated anonymous volumes when it exits | +| [`--rm`](#rm) | `bool` | | Automatically remove the container and its associated anonymous volumes when it exits | | `--runtime` | `string` | | Runtime to use for this container | | [`--security-opt`](#security-opt) | `list` | | Security Options | | `--shm-size` | `bytes` | `0` | Size of /dev/shm | @@ -105,7 +105,7 @@ Create and run a new container from an image | [`--storage-opt`](#storage-opt) | `list` | | Storage driver options for the container | | [`--sysctl`](#sysctl) | `map` | `map[]` | Sysctl options | | [`--tmpfs`](#tmpfs) | `list` | | Mount a tmpfs directory | -| [`-t`](#tty), [`--tty`](#tty) | | | Allocate a pseudo-TTY | +| [`-t`](#tty), [`--tty`](#tty) | `bool` | | Allocate a pseudo-TTY | | [`--ulimit`](#ulimit) | `ulimit` | | Ulimit options | | `-u`, `--user` | `string` | | Username or UID (format: [:]) | | [`--userns`](#userns) | `string` | | User namespace to use | @@ -291,8 +291,7 @@ running processes in that namespace. By default, all containers, including those with `--network=host`, have their own UTS namespace. Setting `--uts` to `host` results in the container using the same UTS namespace as the host. -> **Note** -> +> [!NOTE] > Docker disallows combining the `--hostname` and `--domainname` flags with > `--uts=host`. This is to prevent containers running in the host's UTS > namespace from attempting to change the hosts' configuration. @@ -350,8 +349,7 @@ In other words, the container can then do almost everything that the host can do. This flag exists to allow special use-cases, like running Docker within Docker. -> **Warning** -> +> [!WARNING] > Use the `--privileged` flag with caution. > A container with `--privileged` is not a securely sandboxed process. > Containers in this mode can get a root shell on the host @@ -363,7 +361,7 @@ Docker. > for example by adding individual kernel capabilities with `--cap-add`. > > For more information, see -> [Runtime privilege and Linux capabilities](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities) +> [Runtime privilege and Linux capabilities](https://docs.docker.com/engine/containers/run/#runtime-privilege-and-linux-capabilities) { .warning } The following example doesn't work, because by default, Docker drops most @@ -533,8 +531,7 @@ host. You can also specify `udp` and `sctp` ports. The [Networking overview page](https://docs.docker.com/network/) explains in detail how to publish ports with Docker. -> **Note** -> +> [!NOTE] > If you don't specify an IP address (i.e., `-p 80:80` instead of `-p > 127.0.0.1:80:80`) when publishing a container's ports, Docker publishes the > port on all interfaces (address `0.0.0.0`) by default. These ports are @@ -715,8 +712,7 @@ or name. For `overlay` networks or custom plugins that support multi-host connectivity, containers connected to the same multi-host network but launched from different Engines can also communicate in this way. -> **Note** -> +> [!NOTE] > The default bridge network only allows containers to communicate with each other using > internal IP addresses. User-created bridge networks provide DNS resolution between > containers using container names. @@ -784,8 +780,7 @@ $ docker network create --subnet 192.0.2.0/24 my-net $ docker run -itd --network=name=my-net,\"driver-opt=com.docker.network.endpoint.sysctls=net.ipv4.conf.IFNAME.log_martians=1,net.ipv4.conf.IFNAME.forwarding=0\",ip=192.0.2.42 busybox ``` -> **Note** -> +> [!NOTE] > Network drivers may restrict the sysctl settings that can be modified and, to protect > the operation of the network, new restrictions may be added in the future. @@ -869,7 +864,7 @@ the following: These `a`, `ctrl-a`, `X`, or `ctrl-\\` values are all examples of valid key sequences. To configure a different configuration default key sequence for all -containers, see [**Configuration file** section](https://docs.docker.com/engine/reference/commandline/cli/#configuration-files). +containers, see [**Configuration file** section](https://docs.docker.com/reference/cli/docker/#configuration-files). ### Add host device to container (--device) @@ -912,8 +907,7 @@ $ docker run --device=/dev/sda:/dev/xvdc:m --rm -it ubuntu fdisk /dev/xvdc fdisk: unable to open /dev/xvdc: Operation not permitted ``` -> **Note** -> +> [!NOTE] > The `--device` option cannot be safely used with ephemeral devices. You shouldn't > add block devices that may be removed to untrusted containers with `--device`. @@ -935,15 +929,13 @@ ports on the host visible in the container. PS C:\> docker run --device=class/86E0D1E0-8089-11D0-9CE4-08003E301F73 mcr.microsoft.com/windows/servercore:ltsc2019 ``` -> **Note** -> +> [!NOTE] > The `--device` option is only supported on process-isolated Windows containers, > and produces an error if the container isolation is `hyperv`. #### CDI devices -> **Note** -> +> [!NOTE] > The CDI feature is experimental, and potentially subject to change. > CDI is currently only supported for Linux containers. @@ -1010,8 +1002,7 @@ ID once the container has finished running. $ cat somefile | docker run -i -a stdin mybuilder dobuild ``` -> **Note** -> +> [!NOTE] > A process running as PID 1 inside a container is treated specially by > Linux: it ignores any signal with the default action. So, the process > doesn't terminate on `SIGINT` or `SIGTERM` unless it's coded to do so. @@ -1124,7 +1115,8 @@ $ docker run -d --device-cgroup-rule='c 42:* rmw' --name my-container my-image Then, a user could ask `udev` to execute a script that would `docker exec my-container mknod newDevX c 42 ` the required device when it is added. -> **Note**: You still need to explicitly add initially present devices to the +> [!NOTE] +> You still need to explicitly add initially present devices to the > `docker run` / `docker create` command. ### Access an NVIDIA GPU @@ -1132,8 +1124,7 @@ the required device when it is added. The `--gpus` flag allows you to access NVIDIA GPU resources. First you need to install the [nvidia-container-runtime](https://nvidia.github.io/nvidia-container-runtime/). -> **Note** -> +> [!NOTE] > You can also specify a GPU as a CDI device with the `--device` flag, see > [CDI devices](#cdi-devices). @@ -1166,12 +1157,12 @@ Use the `--restart` flag to specify a container's *restart policy*. A restart policy controls whether the Docker daemon restarts a container after exit. Docker supports the following restart policies: -| Policy | Result | -|:---------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `no` | Do not automatically restart the container when it exits. This is the default. | -| `on-failure[:max-retries]` | Restart only if the container exits with a non-zero exit status. Optionally, limit the number of restart retries the Docker daemon attempts. | -| `unless-stopped` | Restart the container unless it's explicitly stopped or Docker itself is stopped or restarted. | -| `always` | Always restart the container regardless of the exit status. When you specify always, the Docker daemon tries to restart the container indefinitely. The container always starts on daemon startup, regardless of the current state of the container. | +| Flag | Description | +| :------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `no` | Don't automatically restart the container. (Default) | +| `on-failure[:max-retries]` | Restart the container if it exits due to an error, which manifests as a non-zero exit code. Optionally, limit the number of times the Docker daemon attempts to restart the container using the `:max-retries` option. The `on-failure` policy only prompts a restart if the container exits with a failure. It doesn't restart the container if the daemon restarts. | +| `always` | Always restart the container if it stops. If it's manually stopped, it's restarted only when Docker daemon restarts or the container itself is manually restarted. | +| `unless-stopped` | Similar to `always`, except that when the container is stopped (manually or otherwise), it isn't restarted even after Docker daemon restarts. | ```console $ docker run --restart=always redis @@ -1243,11 +1234,10 @@ the container and remove the file system when the container exits, use the `--rm` flag: ```text ---rm=false: Automatically remove the container when it exits +--rm: Automatically remove the container when it exits ``` -> **Note** -> +> [!NOTE] > If you set the `--rm` flag, Docker also removes the anonymous volumes > associated with the container when the container is removed. This is similar > to running `docker rm -v my-container`. Only volumes that are specified @@ -1293,7 +1283,7 @@ connect to services running on the host machine. It's conventional to use `host.docker.internal` as the hostname referring to `host-gateway`. Docker Desktop automatically resolves this hostname, see -[Explore networking features](https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host). +[Explore networking features](https://docs.docker.com/desktop/features/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host). The following example shows how the special `host-gateway` value works. The example runs an HTTP server that serves a file from host to container over the @@ -1322,7 +1312,7 @@ the `--log-driver=` with the `docker run` command to configure the container's logging driver. To learn about the supported logging drivers and how to use them, refer to -[Configure logging drivers](https://docs.docker.com/config/containers/logging/configure/). +[Configure logging drivers](https://docs.docker.com/engine/logging/configure/). To disable logging for a container, set the `--log-driver` flag to `none`: @@ -1345,14 +1335,12 @@ $ docker run --ulimit nofile=1024:1024 --rm debian sh -c "ulimit -n" 1024 ``` -> **Note** -> +> [!NOTE] > If you don't provide a hard limit value, Docker uses the soft limit value > for both values. If you don't provide any values, they are inherited from > the default `ulimits` set on the daemon. -> **Note** -> +> [!NOTE] > The `as` option is deprecated. > In other words, the following script is not supported: > @@ -1417,8 +1405,7 @@ the same content between containers. $ docker run --security-opt label=level:s0:c100,c200 -it fedora bash ``` -> **Note** -> +> [!NOTE] > Automatic translation of MLS labels isn't supported. To disable the security labeling for a container entirely, you can use @@ -1436,8 +1423,7 @@ that's only allowed to listen on Apache ports: $ docker run --security-opt label=type:svirt_apache_t -it ubuntu bash ``` -> **Note** -> +> [!NOTE] > You would have to write policy defining a `svirt_apache_t` type. To prevent your container processes from gaining additional privileges, you can @@ -1558,8 +1544,7 @@ network namespace, run this command: $ docker run --sysctl net.ipv4.ip_forward=1 someimage ``` -> **Note** -> +> [!NOTE] > Not all sysctls are namespaced. Docker does not support changing sysctls > inside of a container that also modify the host system. As the kernel > evolves we expect to see more sysctls become namespaced. diff --git a/docs/reference/commandline/container_start.md b/docs/reference/commandline/container_start.md index 866a24921aee..256fc9c8a830 100644 --- a/docs/reference/commandline/container_start.md +++ b/docs/reference/commandline/container_start.md @@ -11,11 +11,11 @@ Start one or more stopped containers | Name | Type | Default | Description | |:----------------------|:---------|:--------|:----------------------------------------------------| -| `-a`, `--attach` | | | Attach STDOUT/STDERR and forward signals | +| `-a`, `--attach` | `bool` | | Attach STDOUT/STDERR and forward signals | | `--checkpoint` | `string` | | Restore from this checkpoint | | `--checkpoint-dir` | `string` | | Use a custom checkpoint storage directory | | `--detach-keys` | `string` | | Override the key sequence for detaching a container | -| `-i`, `--interactive` | | | Attach container's STDIN | +| `-i`, `--interactive` | `bool` | | Attach container's STDIN | diff --git a/docs/reference/commandline/container_stats.md b/docs/reference/commandline/container_stats.md index f59ea7838a77..c1a9a44e9134 100644 --- a/docs/reference/commandline/container_stats.md +++ b/docs/reference/commandline/container_stats.md @@ -11,10 +11,10 @@ Display a live stream of container(s) resource usage statistics | Name | Type | Default | Description | |:----------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `-a`, `--all` | | | Show all containers (default shows just running) | +| `-a`, `--all` | `bool` | | Show all containers (default shows just running) | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `--no-stream` | | | Disable streaming stats and only pull the first result | -| `--no-trunc` | | | Do not truncate output | +| `--no-stream` | `bool` | | Disable streaming stats and only pull the first result | +| `--no-trunc` | `bool` | | Do not truncate output | @@ -29,8 +29,7 @@ containers do not return any data. If you need more detailed information about a container's resource usage, use the `/containers/(id)/stats` API endpoint. -> **Note** -> +> [!NOTE] > On Linux, the Docker CLI reports memory usage by subtracting cache usage from > the total memory usage. The API does not perform such a calculation but rather > provides the total memory usage and the amount from the cache so that clients @@ -41,8 +40,7 @@ the `/containers/(id)/stats` API endpoint. > field. On cgroup v2 hosts, the cache usage is defined as the value of > `inactive_file` field. -> **Note** -> +> [!NOTE] > The `PIDS` column contains the number of processes and kernel threads created > by that container. Threads is the term used by Linux kernel. Other equivalent > terms are "lightweight process" or "kernel task", etc. A large number in the diff --git a/docs/reference/commandline/container_stop.md b/docs/reference/commandline/container_stop.md index d46148356ba5..78f03253810c 100644 --- a/docs/reference/commandline/container_stop.md +++ b/docs/reference/commandline/container_stop.md @@ -9,10 +9,10 @@ Stop one or more running containers ### Options -| Name | Type | Default | Description | -|:-----------------|:---------|:--------|:---------------------------------------------| -| `-s`, `--signal` | `string` | | Signal to send to the container | -| `-t`, `--time` | `int` | `0` | Seconds to wait before killing the container | +| Name | Type | Default | Description | +|:---------------------------------------|:---------|:--------|:---------------------------------------------| +| [`-s`](#signal), [`--signal`](#signal) | `string` | | Signal to send to the container | +| [`-t`](#time), [`--time`](#time) | `int` | `0` | Seconds to wait before killing the container | @@ -22,10 +22,40 @@ Stop one or more running containers The main process inside the container will receive `SIGTERM`, and after a grace period, `SIGKILL`. The first signal can be changed with the `STOPSIGNAL` instruction in the container's Dockerfile, or the `--stop-signal` option to -`docker run`. +`docker run` and `docker create`. ## Examples ```console $ docker stop my_container ``` + +### Stop container with signal (-s, --signal) + +The `--signal` flag sends the system call signal to the container to exit. +This signal can be a signal name in the format `SIG`, for instance +`SIGKILL`, or an unsigned number that matches a position in the kernel's +syscall table, for instance `9`. Refer to [signal(7)](https://man7.org/linux/man-pages/man7/signal.7.html) +for available signals. + +The default signal to use is defined by the image's [`StopSignal`](https://github.com/opencontainers/image-spec/blob/v1.1.0/config.md), +which can be set through the [`STOPSIGNAL`](https://docs.docker.com/reference/dockerfile/#stopsignal) +Dockerfile instruction when building the image, or configured using the +[`--stop-signal`](https://docs.docker.com/reference/cli/docker/container/run/#stop-signal) +option when creating the container. If no signal is configured for the +container, `SIGTERM` is used as default. + +### Stop container with timeout (-t, --timeout) + +The `--time` flag sets the number of seconds to wait for the container +to stop after sending the pre-defined (see [`--signal`]{#signal)) system call signal. +If the container does not exit after the timeout elapses, it's forcibly killed +with a `SIGKILL` signal. + +If you set `--time` to `-1`, no timeout is applied, and the daemon +waits indefinitely for the container to exit. + +The default timeout can be specified using the [`--stop-timeout`](https://docs.docker.com/reference/cli/docker/container/run/#stop-timeout) +option when creating the container. If no default is configured for the container, +the Daemon determines the default, and is 10 seconds for Linux containers, and +30 seconds for Windows containers. diff --git a/docs/reference/commandline/container_update.md b/docs/reference/commandline/container_update.md index a2dbcd7804a0..e31f6fd77a46 100644 --- a/docs/reference/commandline/container_update.md +++ b/docs/reference/commandline/container_update.md @@ -42,8 +42,7 @@ options on a running or a stopped container. On kernel version older than 4.6, you can only update `--kernel-memory` on a stopped container or on a running container with kernel memory initialized. -> **Warning** -> +> [!WARNING] > The `docker update` and `docker container update` commands are not supported > for Windows containers. { .warning } @@ -78,8 +77,7 @@ running container only if the container was started with `--kernel-memory`. If the container was started without `--kernel-memory` you need to stop the container before updating kernel memory. -> **Note** -> +> [!NOTE] > The `--kernel-memory` option has been deprecated since Docker 20.10. For example, if you started a container with this command: diff --git a/docs/reference/commandline/container_wait.md b/docs/reference/commandline/container_wait.md index 6cec6b2a02fa..cc2f73b32b74 100644 --- a/docs/reference/commandline/container_wait.md +++ b/docs/reference/commandline/container_wait.md @@ -10,8 +10,7 @@ Block until one or more containers stop, then print their exit codes -> **Note** -> +> [!NOTE] > `docker wait` returns `0` when run against a container which had already > exited before the `docker wait` command was run. diff --git a/docs/reference/commandline/context_ls.md b/docs/reference/commandline/context_ls.md index e7d9b0ea1532..2f062f4a2456 100644 --- a/docs/reference/commandline/context_ls.md +++ b/docs/reference/commandline/context_ls.md @@ -12,7 +12,7 @@ List contexts | Name | Type | Default | Description | |:----------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `--format` | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `-q`, `--quiet` | | | Only show context names | +| `-q`, `--quiet` | `bool` | | Only show context names | diff --git a/docs/reference/commandline/context_rm.md b/docs/reference/commandline/context_rm.md index 933941580a6f..05ed2f1f3859 100644 --- a/docs/reference/commandline/context_rm.md +++ b/docs/reference/commandline/context_rm.md @@ -9,9 +9,9 @@ Remove one or more contexts ### Options -| Name | Type | Default | Description | -|:----------------|:-----|:--------|:--------------------------------------| -| `-f`, `--force` | | | Force the removal of a context in use | +| Name | Type | Default | Description | +|:----------------|:-------|:--------|:--------------------------------------| +| `-f`, `--force` | `bool` | | Force the removal of a context in use | diff --git a/docs/reference/commandline/cp.md b/docs/reference/commandline/cp.md index 41c9529f9f0a..ded96d696643 100644 --- a/docs/reference/commandline/cp.md +++ b/docs/reference/commandline/cp.md @@ -14,11 +14,11 @@ container source to stdout. ### Options -| Name | Type | Default | Description | -|:----------------------|:-----|:--------|:-------------------------------------------------------------------------------------------------------------| -| `-a`, `--archive` | | | Archive mode (copy all uid/gid information) | -| `-L`, `--follow-link` | | | Always follow symbol link in SRC_PATH | -| `-q`, `--quiet` | | | Suppress progress output during copy. Progress output is automatically suppressed if no terminal is attached | +| Name | Type | Default | Description | +|:----------------------|:-------|:--------|:-------------------------------------------------------------------------------------------------------------| +| `-a`, `--archive` | `bool` | | Archive mode (copy all uid/gid information) | +| `-L`, `--follow-link` | `bool` | | Always follow symbol link in SRC_PATH | +| `-q`, `--quiet` | `bool` | | Suppress progress output during copy. Progress output is automatically suppressed if no terminal is attached | diff --git a/docs/reference/commandline/create.md b/docs/reference/commandline/create.md index f0964921c5e8..59488a5404af 100644 --- a/docs/reference/commandline/create.md +++ b/docs/reference/commandline/create.md @@ -54,10 +54,10 @@ Create a new container | `--health-start-interval` | `duration` | `0s` | Time between running the check during the start period (ms\|s\|m\|h) (default 0s) | | `--health-start-period` | `duration` | `0s` | Start period for the container to initialize before starting health-retries countdown (ms\|s\|m\|h) (default 0s) | | `--health-timeout` | `duration` | `0s` | Maximum time to allow one check to run (ms\|s\|m\|h) (default 0s) | -| `--help` | | | Print usage | +| `--help` | `bool` | | Print usage | | `-h`, `--hostname` | `string` | | Container host name | -| `--init` | | | Run an init inside the container that forwards signals and reaps processes | -| `-i`, `--interactive` | | | Keep STDIN open even if not attached | +| `--init` | `bool` | | Run an init inside the container that forwards signals and reaps processes | +| `-i`, `--interactive` | `bool` | | Keep STDIN open even if not attached | | `--io-maxbandwidth` | `bytes` | `0` | Maximum IO bandwidth limit for the system drive (Windows only) | | `--io-maxiops` | `uint64` | `0` | Maximum IOps limit for the system drive (Windows only) | | `--ip` | `string` | | IPv4 address (e.g., 172.30.100.104) | @@ -80,20 +80,20 @@ Create a new container | `--name` | `string` | | Assign a name to the container | | `--network` | `network` | | Connect a container to a network | | `--network-alias` | `list` | | Add network-scoped alias for the container | -| `--no-healthcheck` | | | Disable any container-specified HEALTHCHECK | -| `--oom-kill-disable` | | | Disable OOM Killer | +| `--no-healthcheck` | `bool` | | Disable any container-specified HEALTHCHECK | +| `--oom-kill-disable` | `bool` | | Disable OOM Killer | | `--oom-score-adj` | `int` | `0` | Tune host's OOM preferences (-1000 to 1000) | | `--pid` | `string` | | PID namespace to use | | `--pids-limit` | `int64` | `0` | Tune container pids limit (set -1 for unlimited) | | `--platform` | `string` | | Set platform if server is multi-platform capable | -| `--privileged` | | | Give extended privileges to this container | +| `--privileged` | `bool` | | Give extended privileges to this container | | `-p`, `--publish` | `list` | | Publish a container's port(s) to the host | -| `-P`, `--publish-all` | | | Publish all exposed ports to random ports | +| `-P`, `--publish-all` | `bool` | | Publish all exposed ports to random ports | | `--pull` | `string` | `missing` | Pull image before creating (`always`, `\|missing`, `never`) | -| `-q`, `--quiet` | | | Suppress the pull output | -| `--read-only` | | | Mount the container's root filesystem as read only | +| `-q`, `--quiet` | `bool` | | Suppress the pull output | +| `--read-only` | `bool` | | Mount the container's root filesystem as read only | | `--restart` | `string` | `no` | Restart policy to apply when a container exits | -| `--rm` | | | Automatically remove the container and its associated anonymous volumes when it exits | +| `--rm` | `bool` | | Automatically remove the container and its associated anonymous volumes when it exits | | `--runtime` | `string` | | Runtime to use for this container | | `--security-opt` | `list` | | Security Options | | `--shm-size` | `bytes` | `0` | Size of /dev/shm | @@ -102,7 +102,7 @@ Create a new container | `--storage-opt` | `list` | | Storage driver options for the container | | `--sysctl` | `map` | `map[]` | Sysctl options | | `--tmpfs` | `list` | | Mount a tmpfs directory | -| `-t`, `--tty` | | | Allocate a pseudo-TTY | +| `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY | | `--ulimit` | `ulimit` | | Ulimit options | | `-u`, `--user` | `string` | | Username or UID (format: [:]) | | `--userns` | `string` | | User namespace to use | diff --git a/docs/reference/commandline/docker.md b/docs/reference/commandline/docker.md index 288afdf8abff..bfa86b8b1d30 100644 --- a/docs/reference/commandline/docker.md +++ b/docs/reference/commandline/docker.md @@ -29,7 +29,7 @@ The base command for the Docker CLI. | [`inspect`](inspect.md) | Return low-level information on Docker objects | | [`kill`](kill.md) | Kill one or more running containers | | [`load`](load.md) | Load an image from a tar archive or STDIN | -| [`login`](login.md) | Log in to a registry | +| [`login`](login.md) | Authenticate to a registry | | [`logout`](logout.md) | Log out from a registry | | [`logs`](logs.md) | Fetch the logs of a container | | [`manifest`](manifest.md) | Manage Docker image manifests and manifest lists | @@ -68,19 +68,415 @@ The base command for the Docker CLI. ### Options -| Name | Type | Default | Description | -|:--------------------|:---------|:-------------------------|:--------------------------------------------------------------------------------------------------------------------------------------| -| `--config` | `string` | `/root/.docker` | Location of client config files | -| `-c`, `--context` | `string` | | Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var and default context set with `docker context use`) | -| `-D`, `--debug` | | | Enable debug mode | -| `-H`, `--host` | `list` | | Daemon socket to connect to | -| `-l`, `--log-level` | `string` | `info` | Set the logging level (`debug`, `info`, `warn`, `error`, `fatal`) | -| `--tls` | | | Use TLS; implied by --tlsverify | -| `--tlscacert` | `string` | `/root/.docker/ca.pem` | Trust certs signed only by this CA | -| `--tlscert` | `string` | `/root/.docker/cert.pem` | Path to TLS certificate file | -| `--tlskey` | `string` | `/root/.docker/key.pem` | Path to TLS key file | -| `--tlsverify` | | | Use TLS and verify the remote | +| Name | Type | Default | Description | +|:---------------------------------|:---------|:-------------------------|:--------------------------------------------------------------------------------------------------------------------------------------| +| `--config` | `string` | `/root/.docker` | Location of client config files | +| `-c`, `--context` | `string` | | Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var and default context set with `docker context use`) | +| `-D`, `--debug` | `bool` | | Enable debug mode | +| [`-H`](#host), [`--host`](#host) | `list` | | Daemon socket to connect to | +| `-l`, `--log-level` | `string` | `info` | Set the logging level (`debug`, `info`, `warn`, `error`, `fatal`) | +| `--tls` | `bool` | | Use TLS; implied by --tlsverify | +| `--tlscacert` | `string` | `/root/.docker/ca.pem` | Trust certs signed only by this CA | +| `--tlscert` | `string` | `/root/.docker/cert.pem` | Path to TLS certificate file | +| `--tlskey` | `string` | `/root/.docker/key.pem` | Path to TLS key file | +| `--tlsverify` | `bool` | | Use TLS and verify the remote | +## Description + +Depending on your Docker system configuration, you may be required to preface +each `docker` command with `sudo`. To avoid having to use `sudo` with the +`docker` command, your system administrator can create a Unix group called +`docker` and add users to it. + +For more information about installing Docker or `sudo` configuration, refer to +the [installation](https://docs.docker.com/install/) instructions for your operating system. + +### Display help text + +To list the help on any command just execute the command, followed by the +`--help` option. + +```console +$ docker run --help + +Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...] + +Create and run a new container from an image + +Options: + --add-host value Add a custom host-to-IP mapping (host:ip) (default []) + -a, --attach value Attach to STDIN, STDOUT or STDERR (default []) +<...> +``` + +### Environment variables + +The following list of environment variables are supported by the `docker` command +line: + +| Variable | Description | +| :---------------------------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `DOCKER_API_VERSION` | Override the negotiated API version to use for debugging (e.g. `1.19`) | +| `DOCKER_CERT_PATH` | Location of your authentication keys. This variable is used both by the `docker` CLI and the [`dockerd` daemon](https://docs.docker.com/reference/cli/dockerd/) | +| `DOCKER_CONFIG` | The location of your client configuration files. | +| `DOCKER_CONTENT_TRUST_SERVER` | The URL of the Notary server to use. Defaults to the same URL as the registry. | +| `DOCKER_CONTENT_TRUST` | When set Docker uses notary to sign and verify images. Equates to `--disable-content-trust=false` for build, create, pull, push, run. | +| `DOCKER_CONTEXT` | Name of the `docker context` to use (overrides `DOCKER_HOST` env var and default context set with `docker context use`) | +| `DOCKER_CUSTOM_HEADERS` | (Experimental) Configure [custom HTTP headers](#custom-http-headers) to be sent by the client. Headers must be provided as a comma-separated list of `name=value` pairs. This is the equivalent to the `HttpHeaders` field in the configuration file. | +| `DOCKER_DEFAULT_PLATFORM` | Default platform for commands that take the `--platform` flag. | +| `DOCKER_HIDE_LEGACY_COMMANDS` | When set, Docker hides "legacy" top-level commands (such as `docker rm`, and `docker pull`) in `docker help` output, and only `Management commands` per object-type (e.g., `docker container`) are printed. This may become the default in a future release. | +| `DOCKER_HOST` | Daemon socket to connect to. | +| `DOCKER_TLS` | Enable TLS for connections made by the `docker` CLI (equivalent of the `--tls` command-line option). Set to a non-empty value to enable TLS. Note that TLS is enabled automatically if any of the other TLS options are set. | +| `DOCKER_TLS_VERIFY` | When set Docker uses TLS and verifies the remote. This variable is used both by the `docker` CLI and the [`dockerd` daemon](https://docs.docker.com/reference/cli/dockerd/) | +| `BUILDKIT_PROGRESS` | Set type of progress output (`auto`, `plain`, `tty`, `rawjson`) when [building](https://docs.docker.com/reference/cli/docker/image/build/) with [BuildKit backend](https://docs.docker.com/build/buildkit/). Use plain to show container output (default `auto`). | + +Because Docker is developed using Go, you can also use any environment +variables used by the Go runtime. In particular, you may find these useful: + +| Variable | Description | +|:--------------|:-------------------------------------------------------------------------------| +| `HTTP_PROXY` | Proxy URL for HTTP requests unless overridden by NoProxy. | +| `HTTPS_PROXY` | Proxy URL for HTTPS requests unless overridden by NoProxy. | +| `NO_PROXY` | Comma-separated values specifying hosts that should be excluded from proxying. | + +See the [Go specification](https://pkg.go.dev/golang.org/x/net/http/httpproxy#Config) +for details on these variables. + +### Option types + +Single character command line options can be combined, so rather than +typing `docker run -i -t --name test busybox sh`, +you can write `docker run -it --name test busybox sh`. + +#### Boolean + +Boolean options take the form `-d=false`. The value you see in the help text is +the default value which is set if you do **not** specify that flag. If you +specify a Boolean flag without a value, this will set the flag to `true`, +irrespective of the default value. + +For example, running `docker run -d` will set the value to `true`, so your +container **will** run in "detached" mode, in the background. + +Options which default to `true` (e.g., `docker build --rm=true`) can only be +set to the non-default value by explicitly setting them to `false`: + +```console +$ docker build --rm=false . +``` + +#### Multi + +You can specify options like `-a=[]` multiple times in a single command line, +for example in these commands: + +```console +$ docker run -a stdin -a stdout -i -t ubuntu /bin/bash + +$ docker run -a stdin -a stdout -a stderr ubuntu /bin/ls +``` + +Sometimes, multiple options can call for a more complex value string as for +`-v`: + +```console +$ docker run -v /host:/container example/mysql +``` + +> [!NOTE] +> Do not use the `-t` and `-a stderr` options together due to +> limitations in the `pty` implementation. All `stderr` in `pty` mode +> simply goes to `stdout`. + +#### Strings and Integers + +Options like `--name=""` expect a string, and they +can only be specified once. Options like `-c=0` +expect an integer, and they can only be specified once. + +### Configuration files + +By default, the Docker command line stores its configuration files in a +directory called `.docker` within your `$HOME` directory. + +Docker manages most of the files in the configuration directory +and you shouldn't modify them. However, you can modify the +`config.json` file to control certain aspects of how the `docker` +command behaves. + +You can modify the `docker` command behavior using environment +variables or command-line options. You can also use options within +`config.json` to modify some of the same behavior. If an environment variable +and the `--config` flag are set, the flag takes precedent over the environment +variable. Command line options override environment variables and environment +variables override properties you specify in a `config.json` file. + +#### Change the `.docker` directory + +To specify a different directory, use the `DOCKER_CONFIG` +environment variable or the `--config` command line option. If both are +specified, then the `--config` option overrides the `DOCKER_CONFIG` environment +variable. The example below overrides the `docker ps` command using a +`config.json` file located in the `~/testconfigs/` directory. + +```console +$ docker --config ~/testconfigs/ ps +``` + +This flag only applies to whatever command is being ran. For persistent +configuration, you can set the `DOCKER_CONFIG` environment variable in your +shell (e.g. `~/.profile` or `~/.bashrc`). The example below sets the new +directory to be `HOME/newdir/.docker`. + +```console +$ echo export DOCKER_CONFIG=$HOME/newdir/.docker > ~/.profile +``` + +### Docker CLI configuration file (`config.json`) properties + + + +Use the Docker CLI configuration to customize settings for the `docker` CLI. The +configuration file uses JSON formatting, and properties: + +By default, configuration file is stored in `~/.docker/config.json`. Refer to the +[change the `.docker` directory](#change-the-docker-directory) section to use a +different location. + +> [!WARNING] +> The configuration file and other files inside the `~/.docker` configuration +> directory may contain sensitive information, such as authentication information +> for proxies or, depending on your credential store, credentials for your image +> registries. Review your configuration file's content before sharing with others, +> and prevent committing the file to version control. + +#### Customize the default output format for commands + +These fields lets you customize the default output format for some commands +if no `--format` flag is provided. + +| Property | Description | +| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `configFormat` | Custom default format for `docker config ls` output. See [`docker config ls`](https://docs.docker.com/reference/cli/docker/config/ls/#format) for a list of supported formatting directives. | +| `imagesFormat` | Custom default format for `docker images` / `docker image ls` output. See [`docker images`](https://docs.docker.com/reference/cli/docker/image/ls/#format) for a list of supported formatting directives. | +| `networksFormat` | Custom default format for `docker network ls` output. See [`docker network ls`](https://docs.docker.com/reference/cli/docker/network/ls/#format) for a list of supported formatting directives. | +| `nodesFormat` | Custom default format for `docker node ls` output. See [`docker node ls`](https://docs.docker.com/reference/cli/docker/node/ls/#format) for a list of supported formatting directives. | +| `pluginsFormat` | Custom default format for `docker plugin ls` output. See [`docker plugin ls`](https://docs.docker.com/reference/cli/docker/plugin/ls/#format) for a list of supported formatting directives. | +| `psFormat` | Custom default format for `docker ps` / `docker container ps` output. See [`docker ps`](https://docs.docker.com/reference/cli/docker/container/ls/#format) for a list of supported formatting directives. | +| `secretFormat` | Custom default format for `docker secret ls` output. See [`docker secret ls`](https://docs.docker.com/reference/cli/docker/secret/ls/#format) for a list of supported formatting directives. | +| `serviceInspectFormat` | Custom default format for `docker service inspect` output. See [`docker service inspect`](https://docs.docker.com/reference/cli/docker/service/inspect/#format) for a list of supported formatting directives. | +| `servicesFormat` | Custom default format for `docker service ls` output. See [`docker service ls`](https://docs.docker.com/reference/cli/docker/service/ls/#format) for a list of supported formatting directives. | +| `statsFormat` | Custom default format for `docker stats` output. See [`docker stats`](https://docs.docker.com/reference/cli/docker/container/stats/#format) for a list of supported formatting directives. | +| `tasksFormat` | Custom default format for `docker stack ps` output. See [`docker stack ps`](https://docs.docker.com/reference/cli/docker/stack/ps/#format) for a list of supported formatting directives. | +| `volumesFormat` | Custom default format for `docker volume ls` output. See [`docker volume ls`](https://docs.docker.com/reference/cli/docker/volume/ls/#format) for a list of supported formatting directives. | + +#### Custom HTTP headers + +The property `HttpHeaders` specifies a set of headers to include in all messages +sent from the Docker client to the daemon. Docker doesn't try to interpret or +understand these headers; it simply puts them into the messages. Docker does +not allow these headers to change any headers it sets for itself. + +Alternatively, use the `DOCKER_CUSTOM_HEADERS` [environment variable](#environment-variables), +which is available in v27.1 and higher. This environment-variable is experimental, +and its exact behavior may change. + +#### Credential store options + +The property `credsStore` specifies an external binary to serve as the default +credential store. When this property is set, `docker login` will attempt to +store credentials in the binary specified by `docker-credential-` which +is visible on `$PATH`. If this property isn't set, credentials are stored +in the `auths` property of the CLI configuration file. For more information, +see the [**Credential stores** section in the `docker login` documentation](https://docs.docker.com/reference/cli/docker/login/#credential-stores) + +The property `credHelpers` specifies a set of credential helpers to use +preferentially over `credsStore` or `auths` when storing and retrieving +credentials for specific registries. If this property is set, the binary +`docker-credential-` will be used when storing or retrieving credentials +for a specific registry. For more information, see the +[**Credential helpers** section in the `docker login` documentation](https://docs.docker.com/reference/cli/docker/login/#credential-helpers) + +#### Automatic proxy configuration for containers + +The property `proxies` specifies proxy environment variables to be automatically +set on containers, and set as `--build-arg` on containers used during `docker build`. +A `"default"` set of proxies can be configured, and will be used for any Docker +daemon that the client connects to, or a configuration per host (Docker daemon), +for example, `https://docker-daemon1.example.com`. The following properties can +be set for each environment: + +| Property | Description | +|:---------------|:--------------------------------------------------------------------------------------------------------| +| `httpProxy` | Default value of `HTTP_PROXY` and `http_proxy` for containers, and as `--build-arg` on `docker build` | +| `httpsProxy` | Default value of `HTTPS_PROXY` and `https_proxy` for containers, and as `--build-arg` on `docker build` | +| `ftpProxy` | Default value of `FTP_PROXY` and `ftp_proxy` for containers, and as `--build-arg` on `docker build` | +| `noProxy` | Default value of `NO_PROXY` and `no_proxy` for containers, and as `--build-arg` on `docker build` | +| `allProxy` | Default value of `ALL_PROXY` and `all_proxy` for containers, and as `--build-arg` on `docker build` | + +These settings are used to configure proxy settings for containers only, and not +used as proxy settings for the `docker` CLI or the `dockerd` daemon. Refer to the +[environment variables](#environment-variables) and [HTTP/HTTPS proxy](https://docs.docker.com/engine/daemon/proxy/#httphttps-proxy) +sections for configuring proxy settings for the CLI and daemon. + +> [!WARNING] +> Proxy settings may contain sensitive information (for example, if the proxy +> requires authentication). Environment variables are stored as plain text in +> the container's configuration, and as such can be inspected through the remote +> API or committed to an image when using `docker commit`. +{ .warning } + +#### Default key-sequence to detach from containers + +Once attached to a container, users detach from it and leave it running using +the using `CTRL-p CTRL-q` key sequence. This detach key sequence is customizable +using the `detachKeys` property. Specify a `` value for the +property. The format of the `` is a comma-separated list of either +a letter [a-Z], or the `ctrl-` combined with any of the following: + +* `a-z` (a single lowercase alpha character ) +* `@` (at sign) +* `[` (left bracket) +* `\\` (two backward slashes) +* `_` (underscore) +* `^` (caret) + +Your customization applies to all containers started in with your Docker client. +Users can override your custom or the default key sequence on a per-container +basis. To do this, the user specifies the `--detach-keys` flag with the `docker +attach`, `docker exec`, `docker run` or `docker start` command. + +#### CLI plugin options + +The property `plugins` contains settings specific to CLI plugins. The +key is the plugin name, while the value is a further map of options, +which are specific to that plugin. + +#### Sample configuration file + +Following is a sample `config.json` file to illustrate the format used for +various fields: + +```json +{ + "HttpHeaders": { + "MyHeader": "MyValue" + }, + "psFormat": "table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}", + "imagesFormat": "table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}", + "pluginsFormat": "table {{.ID}}\t{{.Name}}\t{{.Enabled}}", + "statsFormat": "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}", + "servicesFormat": "table {{.ID}}\t{{.Name}}\t{{.Mode}}", + "secretFormat": "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}", + "configFormat": "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}", + "serviceInspectFormat": "pretty", + "nodesFormat": "table {{.ID}}\t{{.Hostname}}\t{{.Availability}}", + "detachKeys": "ctrl-e,e", + "credsStore": "secretservice", + "credHelpers": { + "awesomereg.example.org": "hip-star", + "unicorn.example.com": "vcbait" + }, + "plugins": { + "plugin1": { + "option": "value" + }, + "plugin2": { + "anotheroption": "anothervalue", + "athirdoption": "athirdvalue" + } + }, + "proxies": { + "default": { + "httpProxy": "http://user:pass@example.com:3128", + "httpsProxy": "https://my-proxy.example.com:3129", + "noProxy": "intra.mycorp.example.com", + "ftpProxy": "http://user:pass@example.com:3128", + "allProxy": "socks://example.com:1234" + }, + "https://manager1.mycorp.example.com:2377": { + "httpProxy": "http://user:pass@example.com:3128", + "httpsProxy": "https://my-proxy.example.com:3129" + } + } +} +``` + +#### Experimental features + +Experimental features provide early access to future product functionality. +These features are intended for testing and feedback, and they may change +between releases without warning or can be removed from a future release. + +Starting with Docker 20.10, experimental CLI features are enabled by default, +and require no configuration to enable them. + +#### Notary + +If using your own notary server and a self-signed certificate or an internal +Certificate Authority, you need to place the certificate at +`tls//ca.crt` in your Docker config directory. + +Alternatively you can trust the certificate globally by adding it to your system's +list of root Certificate Authorities. + +## Examples + +### Specify daemon host (-H, --host) + +You can use the `-H`, `--host` flag to specify a socket to use when you invoke +a `docker` command. You can use the following protocols: + +| Scheme | Description | Example | +|----------------------------------------|---------------------------|----------------------------------| +| `unix://[]` | Unix socket (Linux only) | `unix:///var/run/docker.sock` | +| `tcp://[[:port]]` | TCP connection | `tcp://174.17.0.1:2376` | +| `ssh://[username@][:port]` | SSH connection | `ssh://user@192.168.64.5` | +| `npipe://[]` | Named pipe (Windows only) | `npipe:////./pipe/docker_engine` | + +If you don't specify the `-H` flag, and you're not using a custom +[context](https://docs.docker.com/engine/context/working-with-contexts), +commands use the following default sockets: + +- `unix:///var/run/docker.sock` on macOS and Linux +- `npipe:////./pipe/docker_engine` on Windows + +To achieve a similar effect without having to specify the `-H` flag for every +command, you could also [create a context](https://docs.docker.com/reference/cli/docker/context/create/), +or alternatively, use the +[`DOCKER_HOST` environment variable](#environment-variables). + +For more information about the `-H` flag, see +[Daemon socket option](https://docs.docker.com/reference/cli/dockerd/#daemon-socket-option). + +#### Using TCP sockets + +The following example shows how to invoke `docker ps` over TCP, to a remote +daemon with IP address `174.17.0.1`, listening on port `2376`: + +```console +$ docker -H tcp://174.17.0.1:2376 ps +``` + +> [!NOTE] +> By convention, the Docker daemon uses port `2376` for secure TLS connections, +> and port `2375` for insecure, non-TLS connections. + +#### Using SSH sockets + +When you use SSH invoke a command on a remote daemon, the request gets forwarded +to the `/var/run/docker.sock` Unix socket on the SSH host. + +```console +$ docker -H ssh://user@192.168.64.5 ps +``` + +You can optionally specify the location of the socket by appending a path +component to the end of the SSH address. + +```console +$ docker -H ssh://user@192.168.64.5/var/run/docker.sock ps +``` diff --git a/docs/reference/commandline/exec.md b/docs/reference/commandline/exec.md index 67bf20a1376f..b2be3e495825 100644 --- a/docs/reference/commandline/exec.md +++ b/docs/reference/commandline/exec.md @@ -11,13 +11,13 @@ Execute a command in a running container | Name | Type | Default | Description | |:----------------------|:---------|:--------|:-------------------------------------------------------| -| `-d`, `--detach` | | | Detached mode: run command in the background | +| `-d`, `--detach` | `bool` | | Detached mode: run command in the background | | `--detach-keys` | `string` | | Override the key sequence for detaching a container | | `-e`, `--env` | `list` | | Set environment variables | | `--env-file` | `list` | | Read in a file of environment variables | -| `-i`, `--interactive` | | | Keep STDIN open even if not attached | -| `--privileged` | | | Give extended privileges to the command | -| `-t`, `--tty` | | | Allocate a pseudo-TTY | +| `-i`, `--interactive` | `bool` | | Keep STDIN open even if not attached | +| `--privileged` | `bool` | | Give extended privileges to the command | +| `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY | | `-u`, `--user` | `string` | | Username or UID (format: `[:]`) | | `-w`, `--workdir` | `string` | | Working directory inside the container | diff --git a/docs/reference/commandline/history.md b/docs/reference/commandline/history.md index 15a02e9092b5..7705c70bcac8 100644 --- a/docs/reference/commandline/history.md +++ b/docs/reference/commandline/history.md @@ -13,8 +13,8 @@ Show the history of an image |:----------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `--format` | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `-H`, `--human` | `bool` | `true` | Print sizes and dates in human readable format | -| `--no-trunc` | | | Don't truncate output | -| `-q`, `--quiet` | | | Only show image IDs | +| `--no-trunc` | `bool` | | Don't truncate output | +| `-q`, `--quiet` | `bool` | | Only show image IDs | diff --git a/docs/reference/commandline/image_build.md b/docs/reference/commandline/image_build.md index bae3c5a90035..bd9ae8ee3dff 100644 --- a/docs/reference/commandline/image_build.md +++ b/docs/reference/commandline/image_build.md @@ -1,438 +1,123 @@ -# build +# build (legacy builder) Build an image from a Dockerfile ### Aliases -`docker image build`, `docker build`, `docker buildx build`, `docker builder build` +`docker image build`, `docker build`, `docker builder build` ### Options -| Name | Type | Default | Description | -|:------------------------------------|:--------------|:----------|:------------------------------------------------------------------| -| [`--add-host`](#add-host) | `list` | | Add a custom host-to-IP mapping (`host:ip`) | -| [`--build-arg`](#build-arg) | `list` | | Set build-time variables | -| [`--cache-from`](#cache-from) | `stringSlice` | | Images to consider as cache sources | -| [`--cgroup-parent`](#cgroup-parent) | `string` | | Set the parent cgroup for the `RUN` instructions during build | -| `--compress` | | | Compress the build context using gzip | -| `--cpu-period` | `int64` | `0` | Limit the CPU CFS (Completely Fair Scheduler) period | -| `--cpu-quota` | `int64` | `0` | Limit the CPU CFS (Completely Fair Scheduler) quota | -| `-c`, `--cpu-shares` | `int64` | `0` | CPU shares (relative weight) | -| `--cpuset-cpus` | `string` | | CPUs in which to allow execution (0-3, 0,1) | -| `--cpuset-mems` | `string` | | MEMs in which to allow execution (0-3, 0,1) | -| `--disable-content-trust` | `bool` | `true` | Skip image verification | -| [`-f`](#file), [`--file`](#file) | `string` | | Name of the Dockerfile (Default is `PATH/Dockerfile`) | -| `--force-rm` | | | Always remove intermediate containers | -| `--iidfile` | `string` | | Write the image ID to the file | -| [`--isolation`](#isolation) | `string` | | Container isolation technology | -| `--label` | `list` | | Set metadata for an image | -| `-m`, `--memory` | `bytes` | `0` | Memory limit | -| `--memory-swap` | `bytes` | `0` | Swap limit equal to memory plus swap: -1 to enable unlimited swap | -| [`--network`](#network) | `string` | `default` | Set the networking mode for the RUN instructions during build | -| `--no-cache` | | | Do not use cache when building the image | -| `--platform` | `string` | | Set platform if server is multi-platform capable | -| `--pull` | | | Always attempt to pull a newer version of the image | -| `-q`, `--quiet` | | | Suppress the build output and print image ID on success | -| `--rm` | `bool` | `true` | Remove intermediate containers after a successful build | -| [`--security-opt`](#security-opt) | `stringSlice` | | Security options | -| `--shm-size` | `bytes` | `0` | Size of `/dev/shm` | -| [`--squash`](#squash) | | | Squash newly built layers into a single new layer | -| [`-t`](#tag), [`--tag`](#tag) | `list` | | Name and optionally a tag in the `name:tag` format | -| [`--target`](#target) | `string` | | Set the target build stage to build. | -| [`--ulimit`](#ulimit) | `ulimit` | | Ulimit options | +| Name | Type | Default | Description | +|:-----------------------------------------------------------------------------------------------------------------------------------------------------|:--------------|:----------|:------------------------------------------------------------------| +| [`--add-host`](https://docs.docker.com/reference/cli/docker/buildx/build/#add-host) | `list` | | Add a custom host-to-IP mapping (`host:ip`) | +| [`--build-arg`](https://docs.docker.com/reference/cli/docker/buildx/build/#build-arg) | `list` | | Set build-time variables | +| `--cache-from` | `stringSlice` | | Images to consider as cache sources | +| [`--cgroup-parent`](https://docs.docker.com/reference/cli/docker/buildx/build/#cgroup-parent) | `string` | | Set the parent cgroup for the `RUN` instructions during build | +| `--compress` | `bool` | | Compress the build context using gzip | +| `--cpu-period` | `int64` | `0` | Limit the CPU CFS (Completely Fair Scheduler) period | +| `--cpu-quota` | `int64` | `0` | Limit the CPU CFS (Completely Fair Scheduler) quota | +| `-c`, `--cpu-shares` | `int64` | `0` | CPU shares (relative weight) | +| `--cpuset-cpus` | `string` | | CPUs in which to allow execution (0-3, 0,1) | +| `--cpuset-mems` | `string` | | MEMs in which to allow execution (0-3, 0,1) | +| `--disable-content-trust` | `bool` | `true` | Skip image verification | +| [`-f`](https://docs.docker.com/reference/cli/docker/buildx/build/#file), [`--file`](https://docs.docker.com/reference/cli/docker/buildx/build/#file) | `string` | | Name of the Dockerfile (Default is `PATH/Dockerfile`) | +| `--force-rm` | `bool` | | Always remove intermediate containers | +| `--iidfile` | `string` | | Write the image ID to the file | +| [`--isolation`](#isolation) | `string` | | Container isolation technology | +| `--label` | `list` | | Set metadata for an image | +| `-m`, `--memory` | `bytes` | `0` | Memory limit | +| `--memory-swap` | `bytes` | `0` | Swap limit equal to memory plus swap: -1 to enable unlimited swap | +| [`--network`](https://docs.docker.com/reference/cli/docker/buildx/build/#network) | `string` | `default` | Set the networking mode for the RUN instructions during build | +| `--no-cache` | `bool` | | Do not use cache when building the image | +| `--platform` | `string` | | Set platform if server is multi-platform capable | +| `--pull` | `bool` | | Always attempt to pull a newer version of the image | +| `-q`, `--quiet` | `bool` | | Suppress the build output and print image ID on success | +| `--rm` | `bool` | `true` | Remove intermediate containers after a successful build | +| [`--security-opt`](#security-opt) | `stringSlice` | | Security options | +| `--shm-size` | `bytes` | `0` | Size of `/dev/shm` | +| [`--squash`](#squash) | `bool` | | Squash newly built layers into a single new layer | +| [`-t`](https://docs.docker.com/reference/cli/docker/buildx/build/#tag), [`--tag`](https://docs.docker.com/reference/cli/docker/buildx/build/#tag) | `list` | | Name and optionally a tag in the `name:tag` format | +| [`--target`](https://docs.docker.com/reference/cli/docker/buildx/build/#target) | `string` | | Set the target build stage to build. | +| `--ulimit` | `ulimit` | | Ulimit options | ## Description -The `docker build` command builds Docker images from a Dockerfile and a -"context". A build's context is the set of files located in the specified -`PATH` or `URL`. The build process can refer to any of the files in the -context. For example, your build can use a [*COPY*](https://docs.docker.com/reference/dockerfile/#copy) -instruction to reference a file in the context. - -The `URL` parameter can refer to three kinds of resources: Git repositories, -pre-packaged tarball contexts, and plain text files. - -### Git repositories - -When the `URL` parameter points to the location of a Git repository, the -repository acts as the build context. The system recursively fetches the -repository and its submodules. The commit history isn't preserved. A -repository is first pulled into a temporary directory on your local host. After -that succeeds, the command sends the directory to the Docker daemon as the context. -Local copy gives you the ability to access private repositories using local -user credentials, VPNs, and so forth. - -> **Note** +> [!IMPORTANT] +> This page refers to the **legacy implementation** of `docker build`, +> using the legacy (pre-BuildKit) build backend. +> This configuration is only relevant if you're building Windows containers. > -> If the `URL` parameter contains a fragment the system recursively clones -> the repository and its submodules. - -Git URLs accept context configuration in their fragment section, separated by a -colon (`:`). The first part represents the reference that Git checks out, -and can be either a branch, a tag, or a remote reference. The second part -represents a subdirectory inside the repository used as a build -context. - -For example, run this command to use a directory called `docker` in the branch -`container`: +> For information about the default `docker build`, using Buildx, +> see [`docker buildx build`](https://docs.docker.com/reference/cli/docker/build/). -```console -$ docker build https://github.com/docker/rootfs.git#container:docker -``` - -The following table represents all the valid suffixes with their build -contexts: +When building with legacy builder, images are created from a Dockerfile by +running a sequence of [commits](./container_commit.md). This process is +inefficient and slow compared to using BuildKit, which is why this build +strategy is deprecated for all use cases except for building Windows +containers. It's still useful for building Windows containers because BuildKit +doesn't yet have full feature parity for Windows. -| Build Syntax Suffix | Commit Used | Build Context Used | -|--------------------------------|-----------------------|--------------------| -| `myrepo.git` | `refs/heads/master` | `/` | -| `myrepo.git#mytag` | `refs/tags/mytag` | `/` | -| `myrepo.git#mybranch` | `refs/heads/mybranch` | `/` | -| `myrepo.git#pull/42/head` | `refs/pull/42/head` | `/` | -| `myrepo.git#:myfolder` | `refs/heads/master` | `/myfolder` | -| `myrepo.git#master:myfolder` | `refs/heads/master` | `/myfolder` | -| `myrepo.git#mytag:myfolder` | `refs/tags/mytag` | `/myfolder` | -| `myrepo.git#mybranch:myfolder` | `refs/heads/mybranch` | `/myfolder` | +Builds invoked with `docker build` use Buildx (and BuildKit) by default, unless: -### Tarball contexts - -If you pass a URL to a remote tarball, the command sends the URL itself to the -daemon: - -```console -$ docker build http://server/context.tar.gz -``` +- You're running Docker Engine in Windows container mode +- You explicitly opt out of using BuildKit by setting the environment variable `DOCKER_BUILDKIT=0`. -The host running the Docker daemon performs the download operation, -which isn't necessarily the same host that issued the build command. -The Docker daemon fetches `context.tar.gz` and uses it as the -build context. Tarball contexts must be tar archives conforming to the standard -`tar` Unix format and can be compressed with any one of the `xz`, `bzip2`, -`gzip` or `identity` (no compression) formats. +The descriptions on this page only covers information that's exclusive to the +legacy builder, and cases where behavior in the legacy builder deviates from +behavior in BuildKit. For information about features and flags that are common +between the legacy builder and BuildKit, such as `--tag` and `--target`, refer +to the documentation for [`docker buildx build`](https://docs.docker.com/reference/cli/docker/buildx/build/). -### Text files +### Build context with the legacy builder -Instead of specifying a context, you can pass a single `Dockerfile` in the -`URL` or pipe the file in via `STDIN`. To pipe a `Dockerfile` from `STDIN`: - -```console -$ docker build - < Dockerfile -``` - -With PowerShell on Windows, you run: - -```powershell -Get-Content Dockerfile | docker build - -``` - -If you use `STDIN` or specify a `URL` pointing to a plain text file, the daemon -places the contents into a `Dockerfile`, and ignores any `-f`, `--file` -option. In this scenario, there is no context. - -By default the `docker build` command looks for a `Dockerfile` at the root -of the build context. The `-f`, `--file`, option lets you specify the path to -an alternative file to use instead. This is useful in cases that use the same -set of files for multiple builds. The path must be to a file within the -build context. Relative path are interpreted as relative to the root of the -context. - -In most cases, it's best to put each Dockerfile in an empty directory. Then, -add to that directory only the files needed for building the Dockerfile. To -increase the build's performance, you can exclude files and directories by -adding a `.dockerignore` file to that directory as well. For information on -creating one, see the [.dockerignore file](https://docs.docker.com/reference/dockerfile/#dockerignore-file). - -If the Docker client loses connection to the daemon, it cancels the build. -This happens if you interrupt the Docker client with `CTRL-c` or if the Docker -client is killed for any reason. If the build initiated a pull which is still -running at the time the build is cancelled, the client also cancels the pull. - -## Return code - -Successful builds return exit code `0`. When the build fails, the command -returns a non-zero exit code and prints an error message to `STDERR`: - -```console -$ docker build -t fail . - -Sending build context to Docker daemon 2.048 kB -Sending build context to Docker daemon -Step 1/3 : FROM busybox - ---> 4986bf8c1536 -Step 2/3 : RUN exit 13 - ---> Running in e26670ec7a0a -INFO[0000] The command [/bin/sh -c exit 13] returned a non-zero code: 13 -$ echo $? -1 -``` - -See also: - -[*Dockerfile Reference*](https://docs.docker.com/reference/dockerfile/). - -## Examples - -### Build with PATH +The build context is the positional argument you pass when invoking the build +command. In the following example, the context is `.`, meaning current the +working directory. ```console $ docker build . - -Uploading context 10240 bytes -Step 1/3 : FROM busybox -Pulling repository busybox - ---> e9aa60c60128MB/2.284 MB (100%) endpoint: https://cdn-registry-1.docker.io/v1/ -Step 2/3 : RUN ls -lh / - ---> Running in 9c9e81692ae9 -total 24 -drwxr-xr-x 2 root root 4.0K Mar 12 2013 bin -drwxr-xr-x 5 root root 4.0K Oct 19 00:19 dev -drwxr-xr-x 2 root root 4.0K Oct 19 00:19 etc -drwxr-xr-x 2 root root 4.0K Nov 15 23:34 lib -lrwxrwxrwx 1 root root 3 Mar 12 2013 lib64 -> lib -dr-xr-xr-x 116 root root 0 Nov 15 23:34 proc -lrwxrwxrwx 1 root root 3 Mar 12 2013 sbin -> bin -dr-xr-xr-x 13 root root 0 Nov 15 23:34 sys -drwxr-xr-x 2 root root 4.0K Mar 12 2013 tmp -drwxr-xr-x 2 root root 4.0K Nov 15 23:34 usr - ---> b35f4035db3f -Step 3/3 : CMD echo Hello world - ---> Running in 02071fceb21b - ---> f52f38b7823e -Successfully built f52f38b7823e -Removing intermediate container 9c9e81692ae9 -Removing intermediate container 02071fceb21b ``` -This example specifies that the `PATH` is `.`, and so `tar`s all the files in the -local directory and sends them to the Docker daemon. The `PATH` specifies -where to find the files for the "context" of the build on the Docker daemon. -Remember that the daemon could be running on a remote machine and that no -parsing of the Dockerfile happens at the client side (where you're running -`docker build`). That means that all the files at `PATH` are sent, not just -the ones listed to [`ADD`](https://docs.docker.com/reference/dockerfile/#add) -in the Dockerfile. - -The transfer of context from the local machine to the Docker daemon is what the -`docker` client means when you see the "Sending build context" message. - -If you wish to keep the intermediate containers after the build is complete, -you must use `--rm=false`. This doesn't affect the build cache. +When using the legacy builder, the build context is sent over to the daemon in +its entirety. With BuildKit, only the files you use in your builds are +transmitted. The legacy builder doesn't calculate which files it needs +beforehand. This means that for builds with a large context, context transfer +can take a long time, even if you're only using a subset of the files included +in the context. -### Build with URL +When using the legacy builder, it's therefore extra important that you +carefully consider what files you include in the context you specify. Use a +[`.dockerignore`](https://docs.docker.com/build/concepts/context/#dockerignore-files) +file to exclude files and directories that you don't require in your build from +being sent as part of the build context. -```console -$ docker build github.com/creack/docker-firefox -``` - -This clones the GitHub repository, using the cloned repository as context, -and the Dockerfile at the root of the repository. You can -specify an arbitrary Git repository by using the `git://` or `git@` scheme. - -```console -$ docker build -f ctx/Dockerfile http://server/ctx.tar.gz - -Downloading context: http://server/ctx.tar.gz [===================>] 240 B/240 B -Step 1/3 : FROM busybox - ---> 8c2e06607696 -Step 2/3 : ADD ctx/container.cfg / - ---> e7829950cee3 -Removing intermediate container b35224abf821 -Step 3/3 : CMD /bin/ls - ---> Running in fbc63d321d73 - ---> 3286931702ad -Removing intermediate container fbc63d321d73 -Successfully built 377c409b35e4 -``` - -This sends the URL `http://server/ctx.tar.gz` to the Docker daemon, which -downloads and extracts the referenced tarball. The `-f ctx/Dockerfile` -parameter specifies a path inside `ctx.tar.gz` to the `Dockerfile` used -to build the image. Any `ADD` commands in that `Dockerfile` that refer to local -paths must be relative to the root of the contents inside `ctx.tar.gz`. In the -example above, the tarball contains a directory `ctx/`, so the `ADD -ctx/container.cfg /` operation works as expected. - -### Build with `-` - -```console -$ docker build - < Dockerfile -``` +#### Accessing paths outside the build context -This example reads a Dockerfile from `STDIN` without context. Due to the lack of a -context, the command doesn't send contents of any local directory to the Docker daemon. -Since there is no context, a Dockerfile `ADD` only works if it refers to a -remote URL. +The legacy builder will error out if you try to access files outside of the +build context using relative paths in your Dockerfile. -```console -$ docker build - < context.tar.gz +```dockerfile +FROM alpine +COPY ../../some-dir . ``` -This example builds an image for a compressed context read from `STDIN`. -Supported formats are: `bzip2`, `gzip` and `xz`. - -### Use a .dockerignore file - ```console $ docker build . - -Uploading context 18.829 MB -Uploading context -Step 1/2 : FROM busybox - ---> 769b9341d937 -Step 2/2 : CMD echo Hello world - ---> Using cache - ---> 99cc1ad10469 -Successfully built 99cc1ad10469 -$ echo ".git" > .dockerignore -$ docker build . -Uploading context 6.76 MB -Uploading context -Step 1/2 : FROM busybox - ---> 769b9341d937 -Step 2/2 : CMD echo Hello world - ---> Using cache - ---> 99cc1ad10469 -Successfully built 99cc1ad10469 -``` - -This example shows the use of the `.dockerignore` file to exclude the `.git` -directory from the context. You can see its effect in the changed size of the -uploaded context. The builder reference contains detailed information on -[creating a .dockerignore file](https://docs.docker.com/reference/dockerfile/#dockerignore-file). - -When using the [BuildKit backend](https://docs.docker.com/build/buildkit/), -`docker build` searches for a `.dockerignore` file relative to the Dockerfile -name. For example, running `docker build -f myapp.Dockerfile .` first looks -for an ignore file named `myapp.Dockerfile.dockerignore`. If it can't find such a file, -if present, it uses the `.dockerignore` file. Using a Dockerfile based -`.dockerignore` is useful if a project contains multiple Dockerfiles that expect -to ignore different sets of files. - -### Tag an image (-t, --tag) - -```console -$ docker build -t vieux/apache:2.0 . -``` - -This examples builds in the same way as the previous example, but it then tags the resulting -image. The repository name will be `vieux/apache` and the tag `2.0`. - -[Read more about valid tags](image_tag.md). - -You can apply multiple tags to an image. For example, you can apply the `latest` -tag to a newly built image and add another tag that references a specific -version. - -For example, to tag an image both as `whenry/fedora-jboss:latest` and -`whenry/fedora-jboss:v2.1`, use the following: - -```console -$ docker build -t whenry/fedora-jboss:latest -t whenry/fedora-jboss:v2.1 . -``` - -### Specify a Dockerfile (-f, --file) - -```console -$ docker build -f Dockerfile.debug . -``` - -This uses a file called `Dockerfile.debug` for the build instructions -instead of `Dockerfile`. - -```console -$ curl example.com/remote/Dockerfile | docker build -f - . -``` - -The above command uses the current directory as the build context and reads -a Dockerfile from stdin. - -```console -$ docker build -f dockerfiles/Dockerfile.debug -t myapp_debug . -$ docker build -f dockerfiles/Dockerfile.prod -t myapp_prod . -``` - -The above commands build the current build context (as specified by the -`.`) twice. Once using a debug version of a `Dockerfile` and once using a -production version. - -```console -$ cd /home/me/myapp/some/dir/really/deep -$ docker build -f /home/me/myapp/dockerfiles/debug /home/me/myapp -$ docker build -f ../../../../dockerfiles/debug /home/me/myapp -``` - -These two `docker build` commands do the exact same thing. They both use the -contents of the `debug` file instead of looking for a `Dockerfile` and use -`/home/me/myapp` as the root of the build context. Note that `debug` is in the -directory structure of the build context, regardless of how you refer to it on -the command line. - -> **Note** -> -> `docker build` returns a `no such file or directory` error if the -> file or directory doesn't exist in the uploaded context. This may -> happen if there is no context, or if you specify a file that's -> elsewhere on the Host system. The context is limited to the current -> directory (and its children) for security reasons, and to ensure -> repeatable builds on remote Docker hosts. This is also the reason why -> `ADD ../file` doesn't work. - -### Use a custom parent cgroup (--cgroup-parent) - -When you run `docker build` with the `--cgroup-parent` option, the daemon runs the containers -used in the build with the [corresponding `docker run` flag](container_run.md#cgroup-parent). - -### Set ulimits in container (--ulimit) - -Using the `--ulimit` option with `docker build` causes the daemon to start each build step's -container using those [`--ulimit` flag values](container_run.md#ulimit). - -### Set build-time variables (--build-arg) - -You can use `ENV` instructions in a Dockerfile to define variable values. These -values persist in the built image. Often persistence isn't what you want. Users -want to specify variables differently depending on which host they build an -image on. - -A good example is `http_proxy` or source versions for pulling intermediate -files. The `ARG` instruction lets Dockerfile authors define values that users -can set at build-time using the `--build-arg` flag: - -```console -$ docker build --build-arg HTTP_PROXY=http://10.20.30.2:1234 --build-arg FTP_PROXY=http://40.50.60.5:4567 . -``` - -This flag allows you to pass the build-time variables that are -accessed like regular environment variables in the `RUN` instruction of the -Dockerfile. These values don't persist in the intermediate or final images -like `ENV` values do. You must add `--build-arg` for each build argument. - -Using this flag doesn't alter the output you see when the build process echoes the`ARG` lines from the -Dockerfile. - -For detailed information on using `ARG` and `ENV` instructions, see the -[Dockerfile reference](https://docs.docker.com/reference/dockerfile/). - -You can also use the `--build-arg` flag without a value, in which case the daemon -propagates the value from the local environment into the Docker container it's building: - -```console -$ export HTTP_PROXY=http://10.20.30.2:1234 -$ docker build --build-arg HTTP_PROXY . +... +Step 2/2 : COPY ../../some-dir . +COPY failed: forbidden path outside the build context: ../../some-dir () ``` -This example is similar to how `docker run -e` works. Refer to the [`docker run` documentation](container_run.md#env) -for more information. - -### Optional security options (--security-opt) +BuildKit on the other hand strips leading relative paths that traverse outside +of the build context. Re-using the previous example, the path `COPY +../../some-dir .` evaluates to `COPY some-dir .` with BuildKit. -This flag is only supported on a daemon running on Windows, and only supports -the `credentialspec` option. The `credentialspec` must be in the format -`file://spec.txt` or `registry://keyname`. +## Examples ### Specify isolation technology for container (--isolation) @@ -448,207 +133,17 @@ Linux namespaces. On Microsoft Windows, you can specify these values: | `process` | Namespace isolation only. | | `hyperv` | Hyper-V hypervisor partition-based isolation. | -Specifying the `--isolation` flag without a value is the same as setting `--isolation="default"`. - -### Add entries to container hosts file (--add-host) - -You can add other hosts into a build container's `/etc/hosts` file by using one -or more `--add-host` flags. This example adds static addresses for hosts named -`my-hostname` and `my_hostname_v6`: - -```console -$ docker build --add-host my_hostname=8.8.8.8 --add-host my_hostname_v6=2001:4860:4860::8888 . -``` - -If you need your build to connect to services running on the host, you can use -the special `host-gateway` value for `--add-host`. In the following example, -build containers resolve `host.docker.internal` to the host's gateway IP. - -```console -$ docker build --add-host host.docker.internal=host-gateway . -``` - -You can wrap an IPv6 address in square brackets. -`=` and `:` are both valid separators. -Both formats in the following example are valid: - -```console -$ docker build --add-host my-hostname:10.180.0.1 --add-host my-hostname_v6=[2001:4860:4860::8888] . -``` - -### Specifying target build stage (--target) - -When building a Dockerfile with multiple build stages, you can use the `--target` -option to specify an intermediate build stage by name as a final stage for the -resulting image. The daemon skips commands after the target stage. - -```dockerfile -FROM debian AS build-env -# ... - -FROM alpine AS production-env -# ... -``` - -```console -$ docker build -t mybuildimage --target build-env . -``` - -### Custom build outputs (--output) - -> **Note** -> -> This feature requires the BuildKit backend. You can either -> [enable BuildKit](https://docs.docker.com/build/buildkit/#getting-started) or -> use the [buildx](https://github.com/docker/buildx) plugin which provides more -> output type options. - -By default, a local container image is created from the build result. The -`--output` (or `-o`) flag allows you to override this behavior, and specify a -custom exporter. Custom exporters allow you to export the build -artifacts as files on the local filesystem instead of a Docker image, which can -be useful for generating local binaries, code generation etc. - -The value for `--output` is a CSV-formatted string defining the exporter type -and options that supports `local` and `tar` exporters. - -The `local` exporter writes the resulting build files to a directory on the client side. The -`tar` exporter is similar but writes the files as a single tarball (`.tar`). - -If you specify no type, the value defaults to the output directory of the local -exporter. Use a hyphen (`-`) to write the output tarball to standard output -(`STDOUT`). - -The following example builds an image using the current directory (`.`) as a build -context, and exports the files to a directory named `out` in the current directory. -If the directory does not exist, Docker creates the directory automatically: - -```console -$ docker build -o out . -``` - -The example above uses the short-hand syntax, omitting the `type` options, and -thus uses the default (`local`) exporter. The example below shows the equivalent -using the long-hand CSV syntax, specifying both `type` and `dest` (destination -path): - -```console -$ docker build --output type=local,dest=out . -``` - -Use the `tar` type to export the files as a `.tar` archive: - -```console -$ docker build --output type=tar,dest=out.tar . -``` - -The example below shows the equivalent when using the short-hand syntax. In this -case, `-` is specified as destination, which automatically selects the `tar` type, -and writes the output tarball to standard output, which is then redirected to -the `out.tar` file: - -```console -$ docker build -o - . > out.tar -``` - -The `--output` option exports all files from the target stage. A common pattern -for exporting only specific files is to do multi-stage builds and to copy the -desired files to a new scratch stage with [`COPY --from`](https://docs.docker.com/reference/dockerfile/#copy). - -The example, the `Dockerfile` below uses a separate stage to collect the -build artifacts for exporting: - -```dockerfile -FROM golang AS build-stage -RUN go get -u github.com/LK4D4/vndr - -FROM scratch AS export-stage -COPY --from=build-stage /go/bin/vndr / -``` - -When building the Dockerfile with the `-o` option, the command only exports the files from the final -stage to the `out` directory, in this case, the `vndr` binary: - -```console -$ docker build -o out . - -[+] Building 2.3s (7/7) FINISHED - => [internal] load build definition from Dockerfile 0.1s - => => transferring dockerfile: 176B 0.0s - => [internal] load .dockerignore 0.0s - => => transferring context: 2B 0.0s - => [internal] load metadata for docker.io/library/golang:latest 1.6s - => [build-stage 1/2] FROM docker.io/library/golang@sha256:2df96417dca0561bf1027742dcc5b446a18957cd28eba6aa79269f23f1846d3f 0.0s - => => resolve docker.io/library/golang@sha256:2df96417dca0561bf1027742dcc5b446a18957cd28eba6aa79269f23f1846d3f 0.0s - => CACHED [build-stage 2/2] RUN go get -u github.com/LK4D4/vndr 0.0s - => [export-stage 1/1] COPY --from=build-stage /go/bin/vndr / 0.2s - => exporting to client 0.4s - => => copying files 10.30MB 0.3s - -$ ls ./out -vndr -``` - -### Specifying external cache sources (--cache-from) - -> **Note** -> -> This feature requires the BuildKit backend. You can either -> [enable BuildKit](https://docs.docker.com/build/buildkit/#getting-started) or -> use the [buildx](https://github.com/docker/buildx) plugin. The previous -> builder has limited support for reusing cache from pre-pulled images. - -In addition to local build cache, the builder can reuse the cache generated from -previous builds with the `--cache-from` flag pointing to an image in the registry. - -To use an image as a cache source, cache metadata needs to be written into the -image on creation. You can do this by setting `--build-arg BUILDKIT_INLINE_CACHE=1` -when building the image. After that, you can use the built image as a cache source -for subsequent builds. - -Upon importing the cache, the builder only pulls the JSON metadata from the -registry and determine possible cache hits based on that information. If there -is a cache hit, the builder pulls the matched layers into the local environment. - -In addition to images, the cache can also be pulled from special cache manifests -generated by [`buildx`](https://github.com/docker/buildx) or the BuildKit CLI -(`buildctl`). These manifests (when built with the `type=registry` and `mode=max` -options) allow pulling layer data for intermediate stages in multi-stage builds. - -The following example builds an image with inline-cache metadata and pushes it -to a registry, then uses the image as a cache source on another machine: - -```console -$ docker build -t myname/myapp --build-arg BUILDKIT_INLINE_CACHE=1 . -$ docker push myname/myapp -``` - -After pushing the image, the image is used as cache source on another machine. -BuildKit automatically pulls the image from the registry if needed. - -On another machine: - -```console -$ docker build --cache-from myname/myapp . -``` - -### Set the networking mode for the RUN instructions during build (--network) - -#### Overview - -Available options for the networking mode are: - -- `default` (default): Run in the default network. -- `none`: Run with no network access. -- `host`: Run in the host’s network environment. +### Optional security options (--security-opt) -Find more details in the [Dockerfile documentation](https://docs.docker.com/reference/dockerfile/#run---network). +This flag is only supported on a daemon running on Windows, and only supports +the `credentialspec` option. The `credentialspec` must be in the format +`file://spec.txt` or `registry://keyname`. ### Squash an image's layers (--squash) (experimental) #### Overview -> **Note** +> [!NOTE] > The `--squash` option is an experimental feature, and should not be considered > stable. diff --git a/docs/reference/commandline/image_history.md b/docs/reference/commandline/image_history.md index 5b8ad00600e1..7ed258a9f5be 100644 --- a/docs/reference/commandline/image_history.md +++ b/docs/reference/commandline/image_history.md @@ -13,8 +13,8 @@ Show the history of an image |:----------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `-H`, `--human` | `bool` | `true` | Print sizes and dates in human readable format | -| `--no-trunc` | | | Don't truncate output | -| `-q`, `--quiet` | | | Only show image IDs | +| `--no-trunc` | `bool` | | Don't truncate output | +| `-q`, `--quiet` | `bool` | | Only show image IDs | diff --git a/docs/reference/commandline/image_load.md b/docs/reference/commandline/image_load.md index 098af0a24335..8b675a3bb1ab 100644 --- a/docs/reference/commandline/image_load.md +++ b/docs/reference/commandline/image_load.md @@ -12,7 +12,7 @@ Load an image from a tar archive or STDIN | Name | Type | Default | Description | |:------------------------------------|:---------|:--------|:---------------------------------------------| | [`-i`](#input), [`--input`](#input) | `string` | | Read from tar archive file, instead of STDIN | -| `-q`, `--quiet` | | | Suppress the load output | +| `-q`, `--quiet` | `bool` | | Suppress the load output | diff --git a/docs/reference/commandline/image_ls.md b/docs/reference/commandline/image_ls.md index 1467c37c6eca..29174f171b46 100644 --- a/docs/reference/commandline/image_ls.md +++ b/docs/reference/commandline/image_ls.md @@ -11,12 +11,13 @@ List images | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `-a`, `--all` | | | Show all images (default hides intermediate images) | -| [`--digests`](#digests) | | | Show digests | +| `-a`, `--all` | `bool` | | Show all images (default hides intermediate images) | +| [`--digests`](#digests) | `bool` | | Show digests | | [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| [`--no-trunc`](#no-trunc) | | | Don't truncate output | -| `-q`, `--quiet` | | | Only show image IDs | +| [`--no-trunc`](#no-trunc) | `bool` | | Don't truncate output | +| `-q`, `--quiet` | `bool` | | Only show image IDs | +| `--tree` | `bool` | | List multi-platform images as a tree (EXPERIMENTAL) | diff --git a/docs/reference/commandline/image_prune.md b/docs/reference/commandline/image_prune.md index 23235dbc18b2..bd02b16275d7 100644 --- a/docs/reference/commandline/image_prune.md +++ b/docs/reference/commandline/image_prune.md @@ -7,9 +7,9 @@ Remove unused images | Name | Type | Default | Description | |:----------------------|:---------|:--------|:-------------------------------------------------| -| `-a`, `--all` | | | Remove all unused images, not just dangling ones | +| `-a`, `--all` | `bool` | | Remove all unused images, not just dangling ones | | [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `until=`) | -| `-f`, `--force` | | | Do not prompt for confirmation | +| `-f`, `--force` | `bool` | | Do not prompt for confirmation | @@ -66,7 +66,7 @@ The currently supported filters are: * label (`label=`, `label==`, `label!=`, or `label!==`) - only remove images with (or without, in case `label!=...` is used) the specified labels. The `until` filter can be Unix timestamps, date formatted -timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed +timestamps, or Go duration strings supported by [ParseDuration](https://pkg.go.dev/time#ParseDuration) (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. Supported formats for date formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`, `2006-01-02T15:04:05.999999999`, `2006-01-02T07:00`, and `2006-01-02`. The local @@ -82,6 +82,7 @@ which removes images with the specified labels. The other format is the `label!=...` (`label!=` or `label!==`), which removes images without the specified labels. +> [!NOTE] > **Predicting what will be removed** > > If you are using positive filtering (testing for the existence of a label or @@ -186,8 +187,7 @@ This example removes images which have a maintainer label not set to `john`: $ docker image prune --filter="label!=maintainer=john" ``` -> **Note** -> +> [!NOTE] > You are prompted for confirmation before the `prune` removes > anything, but you are not shown a list of what will potentially be removed. > In addition, `docker image ls` doesn't support negative filtering, so it diff --git a/docs/reference/commandline/image_pull.md b/docs/reference/commandline/image_pull.md index 8333dd3e8196..3d390a294e49 100644 --- a/docs/reference/commandline/image_pull.md +++ b/docs/reference/commandline/image_pull.md @@ -11,10 +11,10 @@ Download an image from a registry | Name | Type | Default | Description | |:---------------------------------------------|:---------|:--------|:-------------------------------------------------| -| [`-a`](#all-tags), [`--all-tags`](#all-tags) | | | Download all tagged images in the repository | +| [`-a`](#all-tags), [`--all-tags`](#all-tags) | `bool` | | Download all tagged images in the repository | | `--disable-content-trust` | `bool` | `true` | Skip image verification | | `--platform` | `string` | | Set platform if server is multi-platform capable | -| `-q`, `--quiet` | | | Suppress verbose output | +| `-q`, `--quiet` | `bool` | | Suppress verbose output | @@ -99,7 +99,7 @@ the same image tagged with different names. Because they are the same image, their layers are stored only once and do not consume extra disk space. For more information about images, layers, and the content-addressable store, -refer to [understand images, containers, and storage drivers](https://docs.docker.com/storage/storagedriver/). +refer to [understand images, containers, and storage drivers](https://docs.docker.com/engine/storage/drivers/). ### Pull an image by digest (immutable identifier) @@ -107,8 +107,8 @@ refer to [understand images, containers, and storage drivers](https://docs.docke So far, you've pulled images by their name (and "tag"). Using names and tags is a convenient way to work with images. When using tags, you can `docker pull` an image again to make sure you have the most up-to-date version of that image. -For example, `docker pull ubuntu:22.04` pulls the latest version of the Ubuntu -22.04 image. +For example, `docker pull ubuntu:24.04` pulls the latest version of the Ubuntu +24.04 image. In some cases you don't want images to be updated to newer versions, but prefer to use a fixed version of an image. Docker enables you to pull an image by its @@ -117,23 +117,23 @@ of an image to pull. Doing so, allows you to "pin" an image to that version, and guarantee that the image you're using is always the same. To know the digest of an image, pull the image first. Let's pull the latest -`ubuntu:22.04` image from Docker Hub: +`ubuntu:24.04` image from Docker Hub: ```console -$ docker pull ubuntu:22.04 +$ docker pull ubuntu:24.04 -22.04: Pulling from library/ubuntu +24.04: Pulling from library/ubuntu 125a6e411906: Pull complete -Digest: sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d -Status: Downloaded newer image for ubuntu:22.04 -docker.io/library/ubuntu:22.04 +Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30 +Status: Downloaded newer image for ubuntu:24.04 +docker.io/library/ubuntu:24.04 ``` Docker prints the digest of the image after the pull has finished. In the example above, the digest of the image is: ```console -sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d +sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30 ``` Docker also prints the digest of an image when pushing to a registry. This @@ -143,23 +143,22 @@ A digest takes the place of the tag when pulling an image, for example, to pull the above image by digest, run the following command: ```console -$ docker pull ubuntu@sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d +$ docker pull ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30 -docker.io/library/ubuntu@sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d: Pulling from library/ubuntu -Digest: sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d -Status: Image is up to date for ubuntu@sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d -docker.io/library/ubuntu@sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d +docker.io/library/ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30: Pulling from library/ubuntu +Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30 +Status: Image is up to date for ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30 +docker.io/library/ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30 ``` Digest can also be used in the `FROM` of a Dockerfile, for example: ```dockerfile -FROM ubuntu@sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d +FROM ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30 LABEL org.opencontainers.image.authors="some maintainer " ``` -> **Note** -> +> [!NOTE] > Using this feature "pins" an image to a specific version in time. > Docker does therefore not pull updated versions of an image, which may include > security updates. If you want to pull an updated image, you need to change the @@ -215,13 +214,11 @@ shorthand) to see the images that were pulled. The example below shows all the ```console $ docker image ls --filter reference=ubuntu REPOSITORY TAG IMAGE ID CREATED SIZE -ubuntu 18.04 c6ad7e71ba7d 5 weeks ago 63.2MB -ubuntu bionic c6ad7e71ba7d 5 weeks ago 63.2MB -ubuntu 22.04 5ccefbfc0416 2 months ago 78MB -ubuntu focal ff0fea8310f3 2 months ago 72.8MB -ubuntu latest ff0fea8310f3 2 months ago 72.8MB -ubuntu jammy 41ba606c8ab9 3 months ago 79MB -ubuntu 20.04 ba6acccedd29 7 months ago 72.8MB +ubuntu 22.04 8a3cdc4d1ad3 3 weeks ago 77.9MB +ubuntu jammy 8a3cdc4d1ad3 3 weeks ago 77.9MB +ubuntu 24.04 35a88802559d 6 weeks ago 78.1MB +ubuntu latest 35a88802559d 6 weeks ago 78.1MB +ubuntu noble 35a88802559d 6 weeks ago 78.1MB ``` ### Cancel a pull diff --git a/docs/reference/commandline/image_push.md b/docs/reference/commandline/image_push.md index 592d2c3bc71e..91f6a41be1f0 100644 --- a/docs/reference/commandline/image_push.md +++ b/docs/reference/commandline/image_push.md @@ -9,12 +9,12 @@ Upload an image to a registry ### Options -| Name | Type | Default | Description | -|:---------------------------------------------|:---------|:--------|:--------------------------------------------------------------------------------------------------------------------------------------------| -| [`-a`](#all-tags), [`--all-tags`](#all-tags) | | | Push all tags of an image to the repository | -| `--disable-content-trust` | `bool` | `true` | Skip image signing | -| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.
'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) | -| `-q`, `--quiet` | | | Suppress verbose output | +| Name | Type | Default | Description | +|:---------------------------------------------|:---------|:--------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [`-a`](#all-tags), [`--all-tags`](#all-tags) | `bool` | | Push all tags of an image to the repository | +| `--disable-content-trust` | `bool` | `true` | Skip image signing | +| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.
Image index won't be pushed, meaning that other manifests, including attestations won't be preserved.
'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) | +| `-q`, `--quiet` | `bool` | | Suppress verbose output | diff --git a/docs/reference/commandline/image_rm.md b/docs/reference/commandline/image_rm.md index f3b303da0b69..81ff55c6b0c4 100644 --- a/docs/reference/commandline/image_rm.md +++ b/docs/reference/commandline/image_rm.md @@ -9,10 +9,10 @@ Remove one or more images ### Options -| Name | Type | Default | Description | -|:----------------|:-----|:--------|:-------------------------------| -| `-f`, `--force` | | | Force removal of the image | -| `--no-prune` | | | Do not delete untagged parents | +| Name | Type | Default | Description | +|:----------------|:-------|:--------|:-------------------------------| +| `-f`, `--force` | `bool` | | Force removal of the image | +| `--no-prune` | `bool` | | Do not delete untagged parents | diff --git a/docs/reference/commandline/images.md b/docs/reference/commandline/images.md index 1fa9f6a319c5..8d615ec21b10 100644 --- a/docs/reference/commandline/images.md +++ b/docs/reference/commandline/images.md @@ -11,12 +11,13 @@ List images | Name | Type | Default | Description | |:-----------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `-a`, `--all` | | | Show all images (default hides intermediate images) | -| `--digests` | | | Show digests | +| `-a`, `--all` | `bool` | | Show all images (default hides intermediate images) | +| `--digests` | `bool` | | Show digests | | `-f`, `--filter` | `filter` | | Filter output based on conditions provided | | `--format` | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `--no-trunc` | | | Don't truncate output | -| `-q`, `--quiet` | | | Only show image IDs | +| `--no-trunc` | `bool` | | Don't truncate output | +| `-q`, `--quiet` | `bool` | | Only show image IDs | +| `--tree` | `bool` | | List multi-platform images as a tree (EXPERIMENTAL) | diff --git a/docs/reference/commandline/index.md b/docs/reference/commandline/index.md index 7e20fd060802..2ad643c74e90 100644 --- a/docs/reference/commandline/index.md +++ b/docs/reference/commandline/index.md @@ -10,7 +10,7 @@ identifier: "smn_cli_guide" This section contains reference information on using Docker's command line client. Each command has a reference page along with samples. If you are unfamiliar with the command line, you should start by reading about how to [Use -the Docker command line](https://docs.docker.com/engine/reference/commandline/cli/). +the Docker command line](https://docs.docker.com/reference/cli/docker/). You start the Docker daemon with the command line. How you start the daemon affects your Docker containers. For that reason you should also make sure to diff --git a/docs/reference/commandline/inspect.md b/docs/reference/commandline/inspect.md index 04a9f6d3b71b..174c440e6c5f 100644 --- a/docs/reference/commandline/inspect.md +++ b/docs/reference/commandline/inspect.md @@ -8,7 +8,7 @@ Return low-level information on Docker objects | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`-f`](#format), [`--format`](#format) | `string` | | Format output using a custom template:
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| [`-s`](#size), [`--size`](#size) | | | Display total file sizes if the type is container | +| [`-s`](#size), [`--size`](#size) | `bool` | | Display total file sizes if the type is container | | [`--type`](#type) | `string` | | Return JSON for specified type | diff --git a/docs/reference/commandline/load.md b/docs/reference/commandline/load.md index 778d263077dd..c961f06020df 100644 --- a/docs/reference/commandline/load.md +++ b/docs/reference/commandline/load.md @@ -12,7 +12,7 @@ Load an image from a tar archive or STDIN | Name | Type | Default | Description | |:----------------|:---------|:--------|:---------------------------------------------| | `-i`, `--input` | `string` | | Read from tar archive file, instead of STDIN | -| `-q`, `--quiet` | | | Suppress the load output | +| `-q`, `--quiet` | `bool` | | Suppress the load output | diff --git a/docs/reference/commandline/login.md b/docs/reference/commandline/login.md index 6e41586a19f4..91b0d28cb29b 100644 --- a/docs/reference/commandline/login.md +++ b/docs/reference/commandline/login.md @@ -1,60 +1,51 @@ # login -Log in to a registry. -If no server is specified, the default is defined by the daemon. +Authenticate to a registry. +Defaults to Docker Hub if no server is specified. ### Options -| Name | Type | Default | Description | -|:--------------------------------------|:---------|:--------|:-----------------------------| -| `-p`, `--password` | `string` | | Password | -| [`--password-stdin`](#password-stdin) | | | Take the password from stdin | -| `-u`, `--username` | `string` | | Username | +| Name | Type | Default | Description | +|:---------------------------------------------|:---------|:--------|:-----------------------------| +| `-p`, `--password` | `string` | | Password | +| [`--password-stdin`](#password-stdin) | `bool` | | Take the password from stdin | +| [`-u`](#username), [`--username`](#username) | `string` | | Username | ## Description -Log in to a registry. +Authenticate to a registry. -## Examples - -### Login to a self-hosted registry - -If you want to log in to a self-hosted registry you can specify this by -adding the server name. - -```console -$ docker login localhost:8080 -``` - -### Provide a password using STDIN (--password-stdin) - -To run the `docker login` command non-interactively, you can set the -`--password-stdin` flag to provide a password through `STDIN`. Using -`STDIN` prevents the password from ending up in the shell's history, -or log-files. - -The following example reads a password from a file, and passes it to the -`docker login` command using `STDIN`: - -```console -$ cat ~/my_password.txt | docker login --username foo --password-stdin -``` +You can authenticate to any public or private registry for which you have +credentials. Authentication may be required for pulling and pushing images. +Other commands, such as `docker scout` and `docker build`, may also require +authentication to access subscription-only features or data related to your +Docker organization. -### Privileged user requirement +Authentication credentials are stored in the configured [credential +store](#credential-stores). If you use Docker Desktop, credentials are +automatically saved to the native keychain of your operating system. If you're +not using Docker Desktop, you can configure the credential store in the Docker +configuration file, which is located at `$HOME/.docker/config.json` on Linux or +`%USERPROFILE%/.docker/config.json` on Windows. If you don't configure a +credential store, Docker stores credentials in the `config.json` file in a +base64-encoded format. This method is less secure than configuring and using a +credential store. -`docker login` requires you to use `sudo` or be `root`, except when: +`docker login` also supports [credential helpers](#credential-helpers) to help +you handle credentials for specific registries. -- Connecting to a remote daemon, such as a `docker-machine` provisioned `docker engine`. -- The user is added to the `docker` group. This will impact the security of your system; the `docker` group is `root` equivalent. See [Docker Daemon Attack Surface](https://docs.docker.com/engine/security/#docker-daemon-attack-surface) for details. +### Authentication methods -You can log in to any public or private repository for which you have -credentials. When you log in, the command stores credentials in -`$HOME/.docker/config.json` on Linux or `%USERPROFILE%/.docker/config.json` on -Windows, via the procedure described below. +You can authenticate to a registry using a username and access token or +password. Docker Hub also supports a web-based sign-in flow, which signs you in +to your Docker account without entering your password. For Docker Hub, the +`docker login` command uses a device code flow by default, unless the +`--username` flag is specified. The device code flow is a secure way to sign +in. See [Authenticate to Docker Hub using device code](#authenticate-to-docker-hub-with-web-based-login). ### Credential stores @@ -75,6 +66,10 @@ Helpers are available for the following credential stores: - Microsoft Windows Credential Manager - [pass](https://www.passwordstore.org/) +With Docker Desktop, the credential store is already installed and configured +for you. Unless you want to change the credential store used by Docker Desktop, +you can skip the following steps. + #### Configure the credential store You need to specify the credential store in `$HOME/.docker/config.json` @@ -94,22 +89,22 @@ the credentials from the file and run `docker login` again. #### Default behavior By default, Docker looks for the native binary on each of the platforms, i.e. -"osxkeychain" on macOS, "wincred" on windows, and "pass" on Linux. A special -case is that on Linux, Docker will fall back to the "secretservice" binary if -it cannot find the "pass" binary. If none of these binaries are present, it -stores the credentials (i.e. password) in base64 encoding in the config files -described above. +`osxkeychain` on macOS, `wincred` on Windows, and `pass` on Linux. A special +case is that on Linux, Docker will fall back to the `secretservice` binary if +it cannot find the `pass` binary. If none of these binaries are present, it +stores the base64-encoded credentials in the `config.json` configuration file. #### Credential helper protocol -Credential helpers can be any program or script that follows a very simple protocol. -This protocol is heavily inspired by Git, but it differs in the information shared. +Credential helpers can be any program or script that implements the credential +helper protocol. This protocol is inspired by Git, but differs in the +information shared. The helpers always use the first argument in the command to identify the action. There are only three possible values for that argument: `store`, `get`, and `erase`. The `store` command takes a JSON payload from the standard input. That payload carries -the server address, to identify the credential, the user name, and either a password +the server address, to identify the credential, the username, and either a password or an identity token. ```json @@ -149,10 +144,10 @@ will show if there was an issue. ### Credential helpers -Credential helpers are similar to the credential store above, but act as the -designated programs to handle credentials for specific registries. The default -credential store (`credsStore` or the config file itself) will not be used for -operations concerning credentials of the specified registries. +Credential helpers are similar to [credential stores](#credential-stores), but +act as the designated programs to handle credentials for specific registries. +The default credential store will not be used for operations concerning +credentials of the specified registries. #### Configure credential helpers @@ -162,19 +157,93 @@ the credentials from the default store. Credential helpers are specified in a similar way to `credsStore`, but allow for multiple helpers to be configured at a time. Keys specify the registry domain, and values specify the suffix of the program to use -(i.e. everything after `docker-credential-`). -For example: +(i.e. everything after `docker-credential-`). For example: ```json { "credHelpers": { - "registry.example.com": "registryhelper", - "awesomereg.example.org": "hip-star", - "unicorn.example.io": "vcbait" + "myregistry.example.com": "secretservice", + "docker.internal.example": "pass", } } ``` +## Examples + +### Authenticate to Docker Hub with web-based login + +By default, the `docker login` command authenticates to Docker Hub, using a +device code flow. This flow lets you authenticate to Docker Hub without +entering your password. Instead, you visit a URL in your web browser, enter a +code, and authenticate. + +```console +$ docker login + +USING WEB-BASED LOGIN +To sign in with credentials on the command line, use 'docker login -u ' + +Your one-time device confirmation code is: LNFR-PGCJ +Press ENTER to open your browser or submit your device code here: https://login.docker.com/activate + +Waiting for authentication in the browser… +``` + +After entering the code in your browser, you are authenticated to Docker Hub +using the account you're currently signed in with on the Docker Hub website or +in Docker Desktop. If you aren't signed in, you are prompted to sign in after +entering the device code. + +### Authenticate to a self-hosted registry + +If you want to authenticate to a self-hosted registry you can specify this by +adding the server name. + +```console +$ docker login registry.example.com +``` + +By default, the `docker login` command assumes that the registry listens on +port 443 or 80. If the registry listens on a different port, you can specify it +by adding the port number to the server name. + +```console +$ docker login registry.example.com:1337 +``` + +> [!NOTE] +> Registry addresses should not include URL path components, only the hostname +> and (optionally) the port. Registry addresses with URL path components may +> result in an error. For example, `docker login registry.example.com/foo/` +> is incorrect, while `docker login registry.example.com` is correct. +> +> The exception to this rule is the Docker Hub registry, which may use the +> `/v1/` path component in the address for historical reasons. + +### Authenticate to a registry with a username and password + +To authenticate to a registry with a username and password, you can use the +`--username` or `-u` flag. The following example authenticates to Docker Hub +with the username `moby`. The password is entered interactively. + +```console +$ docker login -u moby +``` + +### Provide a password using STDIN (--password-stdin) + +To run the `docker login` command non-interactively, you can set the +`--password-stdin` flag to provide a password through `STDIN`. Using +`STDIN` prevents the password from ending up in the shell's history, +or log-files. + +The following example reads a password from a file, and passes it to the +`docker login` command using `STDIN`: + +```console +$ cat ~/my_password.txt | docker login --username foo --password-stdin +``` + ## Related commands * [logout](logout.md) diff --git a/docs/reference/commandline/logs.md b/docs/reference/commandline/logs.md index 840f0562a5c6..56e7671c5c2a 100644 --- a/docs/reference/commandline/logs.md +++ b/docs/reference/commandline/logs.md @@ -11,11 +11,11 @@ Fetch the logs of a container | Name | Type | Default | Description | |:---------------------|:---------|:--------|:---------------------------------------------------------------------------------------------------| -| `--details` | | | Show extra details provided to logs | -| `-f`, `--follow` | | | Follow log output | +| `--details` | `bool` | | Show extra details provided to logs | +| `-f`, `--follow` | `bool` | | Follow log output | | `--since` | `string` | | Show logs since timestamp (e.g. `2013-01-02T13:23:37Z`) or relative (e.g. `42m` for 42 minutes) | | `-n`, `--tail` | `string` | `all` | Number of lines to show from the end of the logs | -| `-t`, `--timestamps` | | | Show timestamps | +| `-t`, `--timestamps` | `bool` | | Show timestamps | | `--until` | `string` | | Show logs before a timestamp (e.g. `2013-01-02T13:23:37Z`) or relative (e.g. `42m` for 42 minutes) | diff --git a/docs/reference/commandline/manifest.md b/docs/reference/commandline/manifest.md index 2d12c76d9734..682974b23734 100644 --- a/docs/reference/commandline/manifest.md +++ b/docs/reference/commandline/manifest.md @@ -284,8 +284,7 @@ $ docker manifest create --insecure myprivateregistry.mycompany.com/repo/image:1 $ docker manifest push --insecure myprivateregistry.mycompany.com/repo/image:tag ``` -> **Note** -> +> [!NOTE] > The `--insecure` flag is not required to annotate a manifest list, > since annotations are to a locally-stored copy of a manifest list. You may also > skip the `--insecure` flag if you are performing a `docker manifest inspect` diff --git a/docs/reference/commandline/manifest_create.md b/docs/reference/commandline/manifest_create.md index 2879fae6f798..1686ed53e951 100644 --- a/docs/reference/commandline/manifest_create.md +++ b/docs/reference/commandline/manifest_create.md @@ -5,10 +5,10 @@ Create a local manifest list for annotating and pushing to a registry ### Options -| Name | Type | Default | Description | -|:----------------|:-----|:--------|:----------------------------------------------| -| `-a`, `--amend` | | | Amend an existing manifest list | -| `--insecure` | | | Allow communication with an insecure registry | +| Name | Type | Default | Description | +|:----------------|:-------|:--------|:----------------------------------------------| +| `-a`, `--amend` | `bool` | | Amend an existing manifest list | +| `--insecure` | `bool` | | Allow communication with an insecure registry | diff --git a/docs/reference/commandline/manifest_inspect.md b/docs/reference/commandline/manifest_inspect.md index 79c062193cab..cc2da1a8df59 100644 --- a/docs/reference/commandline/manifest_inspect.md +++ b/docs/reference/commandline/manifest_inspect.md @@ -5,10 +5,10 @@ Display an image manifest, or manifest list ### Options -| Name | Type | Default | Description | -|:------------------|:-----|:--------|:-----------------------------------------------------| -| `--insecure` | | | Allow communication with an insecure registry | -| `-v`, `--verbose` | | | Output additional info including layers and platform | +| Name | Type | Default | Description | +|:------------------|:-------|:--------|:-----------------------------------------------------| +| `--insecure` | `bool` | | Allow communication with an insecure registry | +| `-v`, `--verbose` | `bool` | | Output additional info including layers and platform | diff --git a/docs/reference/commandline/manifest_push.md b/docs/reference/commandline/manifest_push.md index 67a7d18a4f1c..34958bd21a2b 100644 --- a/docs/reference/commandline/manifest_push.md +++ b/docs/reference/commandline/manifest_push.md @@ -5,10 +5,10 @@ Push a manifest list to a repository ### Options -| Name | Type | Default | Description | -|:----------------|:-----|:--------|:------------------------------------------| -| `--insecure` | | | Allow push to an insecure registry | -| `-p`, `--purge` | | | Remove the local manifest list after push | +| Name | Type | Default | Description | +|:----------------|:-------|:--------|:------------------------------------------| +| `--insecure` | `bool` | | Allow push to an insecure registry | +| `-p`, `--purge` | `bool` | | Remove the local manifest list after push | diff --git a/docs/reference/commandline/network_connect.md b/docs/reference/commandline/network_connect.md index b8840eb40444..08924b936ec7 100644 --- a/docs/reference/commandline/network_connect.md +++ b/docs/reference/commandline/network_connect.md @@ -80,8 +80,7 @@ sets `net.ipv4.conf.eth3.log_martians=1` and `net.ipv4.conf.eth3.forwarding=0`. $ docker network connect --driver-opt=\"com.docker.network.endpoint.sysctls=net.ipv4.conf.IFNAME.log_martians=1,net.ipv4.conf.IFNAME.forwarding=0\" multi-host-network container2 ``` -> **Note** -> +> [!NOTE] > Network drivers may restrict the sysctl settings that can be modified and, to protect > the operation of the network, new restrictions may be added in the future. diff --git a/docs/reference/commandline/network_create.md b/docs/reference/commandline/network_create.md index 585bc373acdc..43c59ba3d07f 100644 --- a/docs/reference/commandline/network_create.md +++ b/docs/reference/commandline/network_create.md @@ -7,18 +7,18 @@ Create a network | Name | Type | Default | Description | |:--------------------------|:--------------|:----------|:--------------------------------------------------------| -| `--attachable` | | | Enable manual container attachment | +| `--attachable` | `bool` | | Enable manual container attachment | | `--aux-address` | `map` | `map[]` | Auxiliary IPv4 or IPv6 addresses used by Network driver | | `--config-from` | `string` | | The network from which to copy the configuration | -| `--config-only` | | | Create a configuration only network | +| `--config-only` | `bool` | | Create a configuration only network | | `-d`, `--driver` | `string` | `bridge` | Driver to manage the Network | | `--gateway` | `stringSlice` | | IPv4 or IPv6 Gateway for the master subnet | -| [`--ingress`](#ingress) | | | Create swarm routing-mesh network | -| [`--internal`](#internal) | | | Restrict external access to the network | +| [`--ingress`](#ingress) | `bool` | | Create swarm routing-mesh network | +| [`--internal`](#internal) | `bool` | | Restrict external access to the network | | `--ip-range` | `stringSlice` | | Allocate container ip from a sub-range | | `--ipam-driver` | `string` | `default` | IP Address Management Driver | | `--ipam-opt` | `map` | `map[]` | Set IPAM driver specific options | -| `--ipv6` | | | Enable or disable IPv6 networking | +| `--ipv6` | `bool` | | Enable or disable IPv6 networking | | `--label` | `list` | | Set metadata on a network | | `-o`, `--opt` | `map` | `map[]` | Set driver specific options | | `--scope` | `string` | | Control the network's scope | diff --git a/docs/reference/commandline/network_disconnect.md b/docs/reference/commandline/network_disconnect.md index 8ab6b9703531..b09a8faeb65d 100644 --- a/docs/reference/commandline/network_disconnect.md +++ b/docs/reference/commandline/network_disconnect.md @@ -5,9 +5,9 @@ Disconnect a container from a network ### Options -| Name | Type | Default | Description | -|:----------------|:-----|:--------|:-------------------------------------------------| -| `-f`, `--force` | | | Force the container to disconnect from a network | +| Name | Type | Default | Description | +|:----------------|:-------|:--------|:-------------------------------------------------| +| `-f`, `--force` | `bool` | | Force the container to disconnect from a network | diff --git a/docs/reference/commandline/network_inspect.md b/docs/reference/commandline/network_inspect.md index 5025d964d967..1c5dec508150 100644 --- a/docs/reference/commandline/network_inspect.md +++ b/docs/reference/commandline/network_inspect.md @@ -8,7 +8,7 @@ Display detailed information on one or more networks | Name | Type | Default | Description | |:------------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `-f`, `--format` | `string` | | Format output using a custom template:
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| [`-v`](#verbose), [`--verbose`](#verbose) | | | Verbose output for diagnostics | +| [`-v`](#verbose), [`--verbose`](#verbose) | `bool` | | Verbose output for diagnostics | diff --git a/docs/reference/commandline/network_ls.md b/docs/reference/commandline/network_ls.md index bce120c2ae4a..31e1501ac6bc 100644 --- a/docs/reference/commandline/network_ls.md +++ b/docs/reference/commandline/network_ls.md @@ -13,8 +13,8 @@ List networks |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`-f`](#filter), [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `driver=bridge`) | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `--no-trunc` | | | Do not truncate the output | -| `-q`, `--quiet` | | | Only display network IDs | +| `--no-trunc` | `bool` | | Do not truncate the output | +| `-q`, `--quiet` | `bool` | | Only display network IDs | diff --git a/docs/reference/commandline/network_prune.md b/docs/reference/commandline/network_prune.md index 9a0ee1d247cc..4ee4101faf25 100644 --- a/docs/reference/commandline/network_prune.md +++ b/docs/reference/commandline/network_prune.md @@ -8,7 +8,7 @@ Remove all unused networks | Name | Type | Default | Description | |:----------------------|:---------|:--------|:-------------------------------------------------| | [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `until=`) | -| `-f`, `--force` | | | Do not prompt for confirmation | +| `-f`, `--force` | `bool` | | Do not prompt for confirmation | @@ -41,7 +41,7 @@ The currently supported filters are: * label (`label=`, `label==`, `label!=`, or `label!==`) - only remove networks with (or without, in case `label!=...` is used) the specified labels. The `until` filter can be Unix timestamps, date formatted -timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed +timestamps, or Go duration strings supported by [ParseDuration](https://pkg.go.dev/time#ParseDuration) (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. Supported formats for date formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`, `2006-01-02T15:04:05.999999999`, `2006-01-02T07:00`, and `2006-01-02`. The local diff --git a/docs/reference/commandline/network_rm.md b/docs/reference/commandline/network_rm.md index 755d2126fdac..0bbe2e4638fd 100644 --- a/docs/reference/commandline/network_rm.md +++ b/docs/reference/commandline/network_rm.md @@ -9,9 +9,9 @@ Remove one or more networks ### Options -| Name | Type | Default | Description | -|:----------------|:-----|:--------|:-------------------------------------------| -| `-f`, `--force` | | | Do not error if the network does not exist | +| Name | Type | Default | Description | +|:----------------|:-------|:--------|:-------------------------------------------| +| `-f`, `--force` | `bool` | | Do not error if the network does not exist | diff --git a/docs/reference/commandline/node_demote.md b/docs/reference/commandline/node_demote.md index c48355490a79..980da8de2d39 100644 --- a/docs/reference/commandline/node_demote.md +++ b/docs/reference/commandline/node_demote.md @@ -10,8 +10,7 @@ Demote one or more nodes from manager in the swarm Demotes an existing manager so that it is no longer a manager. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the [Swarm mode > section](https://docs.docker.com/engine/swarm/) in the documentation. diff --git a/docs/reference/commandline/node_inspect.md b/docs/reference/commandline/node_inspect.md index cc99d4f74084..ceffb04d3f34 100644 --- a/docs/reference/commandline/node_inspect.md +++ b/docs/reference/commandline/node_inspect.md @@ -8,7 +8,7 @@ Display detailed information on one or more nodes | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`-f`](#format), [`--format`](#format) | `string` | | Format output using a custom template:
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `--pretty` | | | Print the information in a human friendly format | +| `--pretty` | `bool` | | Print the information in a human friendly format | @@ -21,8 +21,7 @@ given template for each result. Go's [text/template](https://pkg.go.dev/text/template) package describes all the details of the format. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/node_ls.md b/docs/reference/commandline/node_ls.md index 823e85994f33..1a8b42de6477 100644 --- a/docs/reference/commandline/node_ls.md +++ b/docs/reference/commandline/node_ls.md @@ -13,7 +13,7 @@ List nodes in the swarm |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `-q`, `--quiet` | | | Only display IDs | +| `-q`, `--quiet` | `bool` | | Only display IDs | @@ -24,8 +24,7 @@ Lists all the nodes that the Docker Swarm manager knows about. You can filter using the `-f` or `--filter` flag. Refer to the [filtering](#filter) section for more information about available filter options. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the @@ -42,8 +41,7 @@ ID HOSTNAME STATUS AVAILABILITY MANAGER STATU e216jshn25ckzbvmwlnh5jr3g * swarm-manager1 Ready Active Leader ``` -> **Note** -> +> [!NOTE] > In the above example output, there is a hidden column of `.Self` that indicates > if the node is the same node as the current docker daemon. A `*` (e.g., > `e216jshn25ckzbvmwlnh5jr3g *`) means this node is the current docker daemon. diff --git a/docs/reference/commandline/node_promote.md b/docs/reference/commandline/node_promote.md index bfb110da9758..223bb3733e9b 100644 --- a/docs/reference/commandline/node_promote.md +++ b/docs/reference/commandline/node_promote.md @@ -10,8 +10,7 @@ Promote one or more nodes to manager in the swarm Promotes a node to manager. This command can only be executed on a manager node. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/node_ps.md b/docs/reference/commandline/node_ps.md index dc79db74236c..fdd21f4f9986 100644 --- a/docs/reference/commandline/node_ps.md +++ b/docs/reference/commandline/node_ps.md @@ -9,9 +9,9 @@ List tasks running on one or more nodes, defaults to current node |:---------------------------------------|:---------|:--------|:-------------------------------------------| | [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Pretty-print tasks using a Go template | -| `--no-resolve` | | | Do not map IDs to Names | -| `--no-trunc` | | | Do not truncate output | -| `-q`, `--quiet` | | | Only display task IDs | +| `--no-resolve` | `bool` | | Do not map IDs to Names | +| `--no-trunc` | `bool` | | Do not truncate output | +| `-q`, `--quiet` | `bool` | | Only display task IDs | @@ -22,8 +22,7 @@ Lists all the tasks on a Node that Docker knows about. You can filter using the `-f` or `--filter` flag. Refer to the [filtering](#filter) section for more information about available filter options. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the @@ -35,11 +34,11 @@ information about available filter options. $ docker node ps swarm-manager1 NAME IMAGE NODE DESIRED STATE CURRENT STATE -redis.1.7q92v0nr1hcgts2amcjyqg3pq redis:3.0.6 swarm-manager1 Running Running 5 hours -redis.6.b465edgho06e318egmgjbqo4o redis:3.0.6 swarm-manager1 Running Running 29 seconds -redis.7.bg8c07zzg87di2mufeq51a2qp redis:3.0.6 swarm-manager1 Running Running 5 seconds -redis.9.dkkual96p4bb3s6b10r7coxxt redis:3.0.6 swarm-manager1 Running Running 5 seconds -redis.10.0tgctg8h8cech4w0k0gwrmr23 redis:3.0.6 swarm-manager1 Running Running 5 seconds +redis.1.7q92v0nr1hcgts2amcjyqg3pq redis:7.4.1 swarm-manager1 Running Running 5 hours +redis.6.b465edgho06e318egmgjbqo4o redis:7.4.1 swarm-manager1 Running Running 29 seconds +redis.7.bg8c07zzg87di2mufeq51a2qp redis:7.4.1 swarm-manager1 Running Running 5 seconds +redis.9.dkkual96p4bb3s6b10r7coxxt redis:7.4.1 swarm-manager1 Running Running 5 seconds +redis.10.0tgctg8h8cech4w0k0gwrmr23 redis:7.4.1 swarm-manager1 Running Running 5 seconds ``` ### Filtering (--filter) @@ -65,11 +64,11 @@ The following filter matches all tasks with a name containing the `redis` string $ docker node ps -f name=redis swarm-manager1 NAME IMAGE NODE DESIRED STATE CURRENT STATE -redis.1.7q92v0nr1hcgts2amcjyqg3pq redis:3.0.6 swarm-manager1 Running Running 5 hours -redis.6.b465edgho06e318egmgjbqo4o redis:3.0.6 swarm-manager1 Running Running 29 seconds -redis.7.bg8c07zzg87di2mufeq51a2qp redis:3.0.6 swarm-manager1 Running Running 5 seconds -redis.9.dkkual96p4bb3s6b10r7coxxt redis:3.0.6 swarm-manager1 Running Running 5 seconds -redis.10.0tgctg8h8cech4w0k0gwrmr23 redis:3.0.6 swarm-manager1 Running Running 5 seconds +redis.1.7q92v0nr1hcgts2amcjyqg3pq redis:7.4.1 swarm-manager1 Running Running 5 hours +redis.6.b465edgho06e318egmgjbqo4o redis:7.4.1 swarm-manager1 Running Running 29 seconds +redis.7.bg8c07zzg87di2mufeq51a2qp redis:7.4.1 swarm-manager1 Running Running 5 seconds +redis.9.dkkual96p4bb3s6b10r7coxxt redis:7.4.1 swarm-manager1 Running Running 5 seconds +redis.10.0tgctg8h8cech4w0k0gwrmr23 redis:7.4.1 swarm-manager1 Running Running 5 seconds ``` #### id @@ -80,7 +79,7 @@ The `id` filter matches a task's id. $ docker node ps -f id=bg8c07zzg87di2mufeq51a2qp swarm-manager1 NAME IMAGE NODE DESIRED STATE CURRENT STATE -redis.7.bg8c07zzg87di2mufeq51a2qp redis:3.0.6 swarm-manager1 Running Running 5 seconds +redis.7.bg8c07zzg87di2mufeq51a2qp redis:7.4.1 swarm-manager1 Running Running 5 seconds ``` #### label @@ -94,8 +93,8 @@ The following filter matches tasks with the `usage` label regardless of its valu $ docker node ps -f "label=usage" NAME IMAGE NODE DESIRED STATE CURRENT STATE -redis.6.b465edgho06e318egmgjbqo4o redis:3.0.6 swarm-manager1 Running Running 10 minutes -redis.7.bg8c07zzg87di2mufeq51a2qp redis:3.0.6 swarm-manager1 Running Running 9 minutes +redis.6.b465edgho06e318egmgjbqo4o redis:7.4.1 swarm-manager1 Running Running 10 minutes +redis.7.bg8c07zzg87di2mufeq51a2qp redis:7.4.1 swarm-manager1 Running Running 9 minutes ``` diff --git a/docs/reference/commandline/node_rm.md b/docs/reference/commandline/node_rm.md index 200adfe38577..0712c619faa9 100644 --- a/docs/reference/commandline/node_rm.md +++ b/docs/reference/commandline/node_rm.md @@ -9,9 +9,9 @@ Remove one or more nodes from the swarm ### Options -| Name | Type | Default | Description | -|:------------------------------------|:-----|:--------|:-----------------------------------| -| [`-f`](#force), [`--force`](#force) | | | Force remove a node from the swarm | +| Name | Type | Default | Description | +|:------------------------------------|:-------|:--------|:-----------------------------------| +| [`-f`](#force), [`--force`](#force) | `bool` | | Force remove a node from the swarm | @@ -20,8 +20,7 @@ Remove one or more nodes from the swarm Removes the specified nodes from a swarm. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/node_update.md b/docs/reference/commandline/node_update.md index 1138ddb5aa22..2fa583aee5a4 100644 --- a/docs/reference/commandline/node_update.md +++ b/docs/reference/commandline/node_update.md @@ -19,8 +19,7 @@ Update a node Update metadata about a node, such as its availability, labels, or roles. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/plugin_create.md b/docs/reference/commandline/plugin_create.md index ff1023406ea1..f68f7cfc3e84 100644 --- a/docs/reference/commandline/plugin_create.md +++ b/docs/reference/commandline/plugin_create.md @@ -5,9 +5,9 @@ Create a plugin from a rootfs and configuration. Plugin data directory must cont ### Options -| Name | Type | Default | Description | -|:-------------|:-----|:--------|:--------------------------------| -| `--compress` | | | Compress the context using gzip | +| Name | Type | Default | Description | +|:-------------|:-------|:--------|:--------------------------------| +| `--compress` | `bool` | | Compress the context using gzip | diff --git a/docs/reference/commandline/plugin_disable.md b/docs/reference/commandline/plugin_disable.md index ec1791155db3..42af48446c11 100644 --- a/docs/reference/commandline/plugin_disable.md +++ b/docs/reference/commandline/plugin_disable.md @@ -5,9 +5,9 @@ Disable a plugin ### Options -| Name | Type | Default | Description | -|:----------------|:-----|:--------|:--------------------------------------| -| `-f`, `--force` | | | Force the disable of an active plugin | +| Name | Type | Default | Description | +|:----------------|:-------|:--------|:--------------------------------------| +| `-f`, `--force` | `bool` | | Force the disable of an active plugin | diff --git a/docs/reference/commandline/plugin_install.md b/docs/reference/commandline/plugin_install.md index 22c3bf2c7cb0..25ceb35c217e 100644 --- a/docs/reference/commandline/plugin_install.md +++ b/docs/reference/commandline/plugin_install.md @@ -8,9 +8,9 @@ Install a plugin | Name | Type | Default | Description | |:--------------------------|:---------|:--------|:--------------------------------------------------| | `--alias` | `string` | | Local name for plugin | -| `--disable` | | | Do not enable the plugin on install | +| `--disable` | `bool` | | Do not enable the plugin on install | | `--disable-content-trust` | `bool` | `true` | Skip image verification | -| `--grant-all-permissions` | | | Grant all permissions necessary to run the plugin | +| `--grant-all-permissions` | `bool` | | Grant all permissions necessary to run the plugin | diff --git a/docs/reference/commandline/plugin_ls.md b/docs/reference/commandline/plugin_ls.md index 894b926fc67c..036aad76c505 100644 --- a/docs/reference/commandline/plugin_ls.md +++ b/docs/reference/commandline/plugin_ls.md @@ -13,8 +13,8 @@ List plugins |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`-f`](#filter), [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `enabled=true`) | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `--no-trunc` | | | Don't truncate output | -| `-q`, `--quiet` | | | Only display plugin IDs | +| `--no-trunc` | `bool` | | Don't truncate output | +| `-q`, `--quiet` | `bool` | | Only display plugin IDs | diff --git a/docs/reference/commandline/plugin_rm.md b/docs/reference/commandline/plugin_rm.md index 1dcd149a3dd9..23ad32f431fd 100644 --- a/docs/reference/commandline/plugin_rm.md +++ b/docs/reference/commandline/plugin_rm.md @@ -9,9 +9,9 @@ Remove one or more plugins ### Options -| Name | Type | Default | Description | -|:----------------|:-----|:--------|:--------------------------------------| -| `-f`, `--force` | | | Force the removal of an active plugin | +| Name | Type | Default | Description | +|:----------------|:-------|:--------|:--------------------------------------| +| `-f`, `--force` | `bool` | | Force the removal of an active plugin | diff --git a/docs/reference/commandline/plugin_set.md b/docs/reference/commandline/plugin_set.md index ec381a4bc1a1..eae492d9bceb 100644 --- a/docs/reference/commandline/plugin_set.md +++ b/docs/reference/commandline/plugin_set.md @@ -100,8 +100,7 @@ $ docker plugin inspect -f '{{with $mount := index .Settings.Mounts 0}}{{$mount. /bar ``` -> **Note** -> +> [!NOTE] > Since only `source` is settable in `mymount`, > `docker plugins set mymount=/bar myplugin` would work too. @@ -122,8 +121,7 @@ $ docker plugin inspect -f '{{with $device := index .Settings.Devices 0}}{{$devi /dev/bar ``` -> **Note** -> +> [!NOTE] > Since only `path` is settable in `mydevice`, > `docker plugins set mydevice=/dev/bar myplugin` would work too. diff --git a/docs/reference/commandline/plugin_upgrade.md b/docs/reference/commandline/plugin_upgrade.md index c0147c86fdf4..39730104d4b7 100644 --- a/docs/reference/commandline/plugin_upgrade.md +++ b/docs/reference/commandline/plugin_upgrade.md @@ -8,8 +8,8 @@ Upgrade an existing plugin | Name | Type | Default | Description | |:--------------------------|:-------|:--------|:----------------------------------------------------------------------| | `--disable-content-trust` | `bool` | `true` | Skip image verification | -| `--grant-all-permissions` | | | Grant all permissions necessary to run the plugin | -| `--skip-remote-check` | | | Do not check if specified remote plugin matches existing plugin image | +| `--grant-all-permissions` | `bool` | | Grant all permissions necessary to run the plugin | +| `--skip-remote-check` | `bool` | | Do not check if specified remote plugin matches existing plugin image | diff --git a/docs/reference/commandline/ps.md b/docs/reference/commandline/ps.md index 57fcc6199ab2..5d1cf16fcf6e 100644 --- a/docs/reference/commandline/ps.md +++ b/docs/reference/commandline/ps.md @@ -11,14 +11,14 @@ List containers | Name | Type | Default | Description | |:-----------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `-a`, `--all` | | | Show all containers (default shows just running) | +| `-a`, `--all` | `bool` | | Show all containers (default shows just running) | | `-f`, `--filter` | `filter` | | Filter output based on conditions provided | | `--format` | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `-n`, `--last` | `int` | `-1` | Show n last created containers (includes all states) | -| `-l`, `--latest` | | | Show the latest created container (includes all states) | -| `--no-trunc` | | | Don't truncate output | -| `-q`, `--quiet` | | | Only display container IDs | -| `-s`, `--size` | | | Display total file sizes | +| `-l`, `--latest` | `bool` | | Show the latest created container (includes all states) | +| `--no-trunc` | `bool` | | Don't truncate output | +| `-q`, `--quiet` | `bool` | | Only display container IDs | +| `-s`, `--size` | `bool` | | Display total file sizes | diff --git a/docs/reference/commandline/pull.md b/docs/reference/commandline/pull.md index f100262fefba..66acb611da1b 100644 --- a/docs/reference/commandline/pull.md +++ b/docs/reference/commandline/pull.md @@ -11,10 +11,10 @@ Download an image from a registry | Name | Type | Default | Description | |:--------------------------|:---------|:--------|:-------------------------------------------------| -| `-a`, `--all-tags` | | | Download all tagged images in the repository | +| `-a`, `--all-tags` | `bool` | | Download all tagged images in the repository | | `--disable-content-trust` | `bool` | `true` | Skip image verification | | `--platform` | `string` | | Set platform if server is multi-platform capable | -| `-q`, `--quiet` | | | Suppress verbose output | +| `-q`, `--quiet` | `bool` | | Suppress verbose output | diff --git a/docs/reference/commandline/push.md b/docs/reference/commandline/push.md index b49467e99609..9558d38e5ebc 100644 --- a/docs/reference/commandline/push.md +++ b/docs/reference/commandline/push.md @@ -9,12 +9,12 @@ Upload an image to a registry ### Options -| Name | Type | Default | Description | -|:--------------------------|:---------|:--------|:--------------------------------------------------------------------------------------------------------------------------------------------| -| `-a`, `--all-tags` | | | Push all tags of an image to the repository | -| `--disable-content-trust` | `bool` | `true` | Skip image signing | -| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.
'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) | -| `-q`, `--quiet` | | | Suppress verbose output | +| Name | Type | Default | Description | +|:--------------------------|:---------|:--------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `-a`, `--all-tags` | `bool` | | Push all tags of an image to the repository | +| `--disable-content-trust` | `bool` | `true` | Skip image signing | +| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.
Image index won't be pushed, meaning that other manifests, including attestations won't be preserved.
'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) | +| `-q`, `--quiet` | `bool` | | Suppress verbose output | diff --git a/docs/reference/commandline/rm.md b/docs/reference/commandline/rm.md index 0c0bf058208e..4f9d92f42255 100644 --- a/docs/reference/commandline/rm.md +++ b/docs/reference/commandline/rm.md @@ -9,11 +9,11 @@ Remove one or more containers ### Options -| Name | Type | Default | Description | -|:------------------|:-----|:--------|:--------------------------------------------------------| -| `-f`, `--force` | | | Force the removal of a running container (uses SIGKILL) | -| `-l`, `--link` | | | Remove the specified link | -| `-v`, `--volumes` | | | Remove anonymous volumes associated with the container | +| Name | Type | Default | Description | +|:------------------|:-------|:--------|:--------------------------------------------------------| +| `-f`, `--force` | `bool` | | Force the removal of a running container (uses SIGKILL) | +| `-l`, `--link` | `bool` | | Remove the specified link | +| `-v`, `--volumes` | `bool` | | Remove anonymous volumes associated with the container | diff --git a/docs/reference/commandline/rmi.md b/docs/reference/commandline/rmi.md index 574fa03114d7..6aac0b757bf5 100644 --- a/docs/reference/commandline/rmi.md +++ b/docs/reference/commandline/rmi.md @@ -9,10 +9,10 @@ Remove one or more images ### Options -| Name | Type | Default | Description | -|:----------------|:-----|:--------|:-------------------------------| -| `-f`, `--force` | | | Force removal of the image | -| `--no-prune` | | | Do not delete untagged parents | +| Name | Type | Default | Description | +|:----------------|:-------|:--------|:-------------------------------| +| `-f`, `--force` | `bool` | | Force removal of the image | +| `--no-prune` | `bool` | | Do not delete untagged parents | diff --git a/docs/reference/commandline/run.md b/docs/reference/commandline/run.md index d7913196efa2..f9e0fbfcf26a 100644 --- a/docs/reference/commandline/run.md +++ b/docs/reference/commandline/run.md @@ -31,7 +31,7 @@ Create and run a new container from an image | `--cpus` | `decimal` | | Number of CPUs | | `--cpuset-cpus` | `string` | | CPUs in which to allow execution (0-3, 0,1) | | `--cpuset-mems` | `string` | | MEMs in which to allow execution (0-3, 0,1) | -| `-d`, `--detach` | | | Run container in background and print container ID | +| `-d`, `--detach` | `bool` | | Run container in background and print container ID | | `--detach-keys` | `string` | | Override the key sequence for detaching a container | | `--device` | `list` | | Add a host device to the container | | `--device-cgroup-rule` | `list` | | Add a rule to the cgroup allowed devices list | @@ -56,10 +56,10 @@ Create and run a new container from an image | `--health-start-interval` | `duration` | `0s` | Time between running the check during the start period (ms\|s\|m\|h) (default 0s) | | `--health-start-period` | `duration` | `0s` | Start period for the container to initialize before starting health-retries countdown (ms\|s\|m\|h) (default 0s) | | `--health-timeout` | `duration` | `0s` | Maximum time to allow one check to run (ms\|s\|m\|h) (default 0s) | -| `--help` | | | Print usage | +| `--help` | `bool` | | Print usage | | `-h`, `--hostname` | `string` | | Container host name | -| `--init` | | | Run an init inside the container that forwards signals and reaps processes | -| `-i`, `--interactive` | | | Keep STDIN open even if not attached | +| `--init` | `bool` | | Run an init inside the container that forwards signals and reaps processes | +| `-i`, `--interactive` | `bool` | | Keep STDIN open even if not attached | | `--io-maxbandwidth` | `bytes` | `0` | Maximum IO bandwidth limit for the system drive (Windows only) | | `--io-maxiops` | `uint64` | `0` | Maximum IOps limit for the system drive (Windows only) | | `--ip` | `string` | | IPv4 address (e.g., 172.30.100.104) | @@ -82,20 +82,20 @@ Create and run a new container from an image | `--name` | `string` | | Assign a name to the container | | `--network` | `network` | | Connect a container to a network | | `--network-alias` | `list` | | Add network-scoped alias for the container | -| `--no-healthcheck` | | | Disable any container-specified HEALTHCHECK | -| `--oom-kill-disable` | | | Disable OOM Killer | +| `--no-healthcheck` | `bool` | | Disable any container-specified HEALTHCHECK | +| `--oom-kill-disable` | `bool` | | Disable OOM Killer | | `--oom-score-adj` | `int` | `0` | Tune host's OOM preferences (-1000 to 1000) | | `--pid` | `string` | | PID namespace to use | | `--pids-limit` | `int64` | `0` | Tune container pids limit (set -1 for unlimited) | | `--platform` | `string` | | Set platform if server is multi-platform capable | -| `--privileged` | | | Give extended privileges to this container | +| `--privileged` | `bool` | | Give extended privileges to this container | | `-p`, `--publish` | `list` | | Publish a container's port(s) to the host | -| `-P`, `--publish-all` | | | Publish all exposed ports to random ports | +| `-P`, `--publish-all` | `bool` | | Publish all exposed ports to random ports | | `--pull` | `string` | `missing` | Pull image before running (`always`, `missing`, `never`) | -| `-q`, `--quiet` | | | Suppress the pull output | -| `--read-only` | | | Mount the container's root filesystem as read only | +| `-q`, `--quiet` | `bool` | | Suppress the pull output | +| `--read-only` | `bool` | | Mount the container's root filesystem as read only | | `--restart` | `string` | `no` | Restart policy to apply when a container exits | -| `--rm` | | | Automatically remove the container and its associated anonymous volumes when it exits | +| `--rm` | `bool` | | Automatically remove the container and its associated anonymous volumes when it exits | | `--runtime` | `string` | | Runtime to use for this container | | `--security-opt` | `list` | | Security Options | | `--shm-size` | `bytes` | `0` | Size of /dev/shm | @@ -105,7 +105,7 @@ Create and run a new container from an image | `--storage-opt` | `list` | | Storage driver options for the container | | `--sysctl` | `map` | `map[]` | Sysctl options | | `--tmpfs` | `list` | | Mount a tmpfs directory | -| `-t`, `--tty` | | | Allocate a pseudo-TTY | +| `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY | | `--ulimit` | `ulimit` | | Ulimit options | | `-u`, `--user` | `string` | | Username or UID (format: [:]) | | `--userns` | `string` | | User namespace to use | diff --git a/docs/reference/commandline/search.md b/docs/reference/commandline/search.md index ddfbb8b3ebf8..d9d653039dc8 100644 --- a/docs/reference/commandline/search.md +++ b/docs/reference/commandline/search.md @@ -10,7 +10,7 @@ Search Docker Hub for images | [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Pretty-print search using a Go template | | [`--limit`](#limit) | `int` | `0` | Max number of search results | -| [`--no-trunc`](#no-trunc) | | | Don't truncate output | +| [`--no-trunc`](#no-trunc) | `bool` | | Don't truncate output | diff --git a/docs/reference/commandline/secret_create.md b/docs/reference/commandline/secret_create.md index 4a508faea1d3..fc2353ccf359 100644 --- a/docs/reference/commandline/secret_create.md +++ b/docs/reference/commandline/secret_create.md @@ -20,8 +20,7 @@ Creates a secret using standard input or from a file for the secret content. For detailed information about using secrets, refer to [manage sensitive data with Docker secrets](https://docs.docker.com/engine/swarm/secrets/). -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/secret_inspect.md b/docs/reference/commandline/secret_inspect.md index ba672bb2e03b..3ae180a43a59 100644 --- a/docs/reference/commandline/secret_inspect.md +++ b/docs/reference/commandline/secret_inspect.md @@ -8,7 +8,7 @@ Display detailed information on one or more secrets | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`-f`](#format), [`--format`](#format) | `string` | | Format output using a custom template:
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `--pretty` | | | Print the information in a human friendly format | +| `--pretty` | `bool` | | Print the information in a human friendly format | @@ -25,8 +25,7 @@ describes all the details of the format. For detailed information about using secrets, refer to [manage sensitive data with Docker secrets](https://docs.docker.com/engine/swarm/secrets/). -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/secret_ls.md b/docs/reference/commandline/secret_ls.md index 53abd742f5a2..2137bbc7e269 100644 --- a/docs/reference/commandline/secret_ls.md +++ b/docs/reference/commandline/secret_ls.md @@ -13,7 +13,7 @@ List secrets |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `-q`, `--quiet` | | | Only display IDs | +| `-q`, `--quiet` | `bool` | | Only display IDs | @@ -24,8 +24,7 @@ Run this command on a manager node to list the secrets in the swarm. For detailed information about using secrets, refer to [manage sensitive data with Docker secrets](https://docs.docker.com/engine/swarm/secrets/). -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/secret_rm.md b/docs/reference/commandline/secret_rm.md index 68d0e002ec21..148975ce7454 100644 --- a/docs/reference/commandline/secret_rm.md +++ b/docs/reference/commandline/secret_rm.md @@ -16,8 +16,7 @@ Removes the specified secrets from the swarm. For detailed information about using secrets, refer to [manage sensitive data with Docker secrets](https://docs.docker.com/engine/swarm/secrets/). -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the @@ -32,8 +31,7 @@ $ docker secret rm secret.json sapth4csdo5b6wz2p5uimh5xg ``` -> **Warning** -> +> [!WARNING] > Unlike `docker rm`, this command does not ask for confirmation before removing > a secret. { .warning } diff --git a/docs/reference/commandline/service.md b/docs/reference/commandline/service.md index 6c8ff4d6be43..9ab7b9b4862c 100644 --- a/docs/reference/commandline/service.md +++ b/docs/reference/commandline/service.md @@ -25,8 +25,7 @@ Manage Swarm services Manage Swarm services. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/service_create.md b/docs/reference/commandline/service_create.md index 32d274694e0e..d059f141831c 100644 --- a/docs/reference/commandline/service_create.md +++ b/docs/reference/commandline/service_create.md @@ -13,7 +13,7 @@ Create a new service | [`--constraint`](#constraint) | `list` | | Placement constraints | | `--container-label` | `list` | | Container labels | | `--credential-spec` | `credential-spec` | | Credential spec for managed service account (Windows only) | -| `-d`, `--detach` | | | Exit immediately instead of waiting for the service to converge | +| `-d`, `--detach` | `bool` | | Exit immediately instead of waiting for the service to converge | | `--dns` | `list` | | Set custom DNS servers | | `--dns-option` | `list` | | Set DNS options | | `--dns-search` | `list` | | Set custom DNS search domains | @@ -31,7 +31,7 @@ Create a new service | `--health-timeout` | `duration` | | Maximum time to allow one check to run (ms\|s\|m\|h) | | `--host` | `list` | | Set one or more custom host-to-IP mappings (host:ip) | | [`--hostname`](#hostname) | `string` | | Container hostname | -| `--init` | | | Use an init inside each service container to forward signals and reap processes | +| `--init` | `bool` | | Use an init inside each service container to forward signals and reap processes | | [`--isolation`](#isolation) | `string` | | Service container isolation mode | | [`-l`](#label), [`--label`](#label) | `list` | | Service labels | | `--limit-cpu` | `decimal` | | Limit CPUs | @@ -44,12 +44,13 @@ Create a new service | [`--mount`](#mount) | `mount` | | Attach a filesystem mount to the service | | `--name` | `string` | | Service name | | [`--network`](#network) | `network` | | Network attachments | -| `--no-healthcheck` | | | Disable any container-specified HEALTHCHECK | -| `--no-resolve-image` | | | Do not query the registry to resolve image digest and supported platforms | +| `--no-healthcheck` | `bool` | | Disable any container-specified HEALTHCHECK | +| `--no-resolve-image` | `bool` | | Do not query the registry to resolve image digest and supported platforms | +| `--oom-score-adj` | `int64` | `0` | Tune host's OOM preferences (-1000 to 1000) | | [`--placement-pref`](#placement-pref) | `pref` | | Add a placement preference | | [`-p`](#publish), [`--publish`](#publish) | `port` | | Publish a port as a node port | -| `-q`, `--quiet` | | | Suppress progress output | -| `--read-only` | | | Mount the container's root filesystem as read only | +| `-q`, `--quiet` | `bool` | | Suppress progress output | +| `--read-only` | `bool` | | Mount the container's root filesystem as read only | | [`--replicas`](#replicas) | `uint` | | Number of tasks | | [`--replicas-max-per-node`](#replicas-max-per-node) | `uint64` | `0` | Maximum number of tasks per node (default 0 = unlimited) | | `--reserve-cpu` | `decimal` | | Reserve CPUs | @@ -68,7 +69,7 @@ Create a new service | `--stop-grace-period` | `duration` | | Time to wait before force killing a container (ns\|us\|ms\|s\|m\|h) (default 10s) | | `--stop-signal` | `string` | | Signal to stop the container | | `--sysctl` | `list` | | Sysctl options | -| `-t`, `--tty` | | | Allocate a pseudo-TTY | +| `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY | | `--ulimit` | `ulimit` | | Ulimit options | | [`--update-delay`](#update-delay) | `duration` | `0s` | Delay between updates (ns\|us\|ms\|s\|m\|h) (default 0s) | | `--update-failure-action` | `string` | | Action on update failure (`pause`, `continue`, `rollback`) (default `pause`) | @@ -77,7 +78,7 @@ Create a new service | `--update-order` | `string` | | Update order (`start-first`, `stop-first`) (default `stop-first`) | | `--update-parallelism` | `uint64` | `1` | Maximum number of tasks updated simultaneously (0 to update all at once) | | `-u`, `--user` | `string` | | Username or UID (format: [:]) | -| [`--with-registry-auth`](#with-registry-auth) | | | Send registry authentication details to swarm agents | +| [`--with-registry-auth`](#with-registry-auth) | `bool` | | Send registry authentication details to swarm agents | | `-w`, `--workdir` | `string` | | Working directory inside the container | @@ -87,8 +88,7 @@ Create a new service Creates a service as described by the specified parameters. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the @@ -99,19 +99,19 @@ Creates a service as described by the specified parameters. ### Create a service ```console -$ docker service create --name redis redis:3.0.6 +$ docker service create --name redis redis:7.4.1 dmu1ept4cxcfe8k8lhtux3ro3 -$ docker service create --mode global --name redis2 redis:3.0.6 +$ docker service create --mode global --name redis2 redis:7.4.1 a8q9dasaafudfs8q8w32udass $ docker service ls ID NAME MODE REPLICAS IMAGE -dmu1ept4cxcf redis replicated 1/1 redis:3.0.6 -a8q9dasaafud redis2 global 1/1 redis:3.0.6 +dmu1ept4cxcf redis replicated 1/1 redis:7.4.1 +a8q9dasaafud redis2 global 1/1 redis:7.4.1 ``` #### Create a service using an image on a private registry (--with-registry-auth) @@ -140,7 +140,7 @@ Use the `--replicas` flag to set the number of replica tasks for a replicated service. The following command creates a `redis` service with `5` replica tasks: ```console -$ docker service create --name redis --replicas=5 redis:3.0.6 +$ docker service create --name redis --replicas=5 redis:7.4.1 4cdgfyky7ozwh3htjfw0d12qv ``` @@ -157,7 +157,7 @@ number of `RUNNING` tasks is `3`: $ docker service ls ID NAME MODE REPLICAS IMAGE -4cdgfyky7ozw redis replicated 3/5 redis:3.0.7 +4cdgfyky7ozw redis replicated 3/5 redis:7.4.1 ``` Once all the tasks are created and `RUNNING`, the actual number of tasks is @@ -167,7 +167,7 @@ equal to the desired number: $ docker service ls ID NAME MODE REPLICAS IMAGE -4cdgfyky7ozw redis replicated 5/5 redis:3.0.7 +4cdgfyky7ozw redis replicated 5/5 redis:7.4.1 ``` ### Create a service with secrets (--secret) @@ -178,7 +178,7 @@ Use the `--secret` flag to give a container access to a Create a service specifying a secret: ```console -$ docker service create --name redis --secret secret.json redis:3.0.6 +$ docker service create --name redis --secret secret.json redis:7.4.1 4cdgfyky7ozwh3htjfw0d12qv ``` @@ -189,7 +189,7 @@ Create a service specifying the secret, target, user/group ID, and mode: $ docker service create --name redis \ --secret source=ssh-key,target=ssh \ --secret source=app-key,target=app,uid=1000,gid=1001,mode=0400 \ - redis:3.0.6 + redis:7.4.1 4cdgfyky7ozwh3htjfw0d12qv ``` @@ -215,14 +215,14 @@ pre-exist in the container. The `mode` is specified as a 4-number sequence such as `0755`. ```console -$ docker service create --name=redis --config redis-conf redis:3.0.6 +$ docker service create --name=redis --config redis-conf redis:7.4.1 ``` Create a service with a config and specify the target location and file mode: ```console $ docker service create --name redis \ - --config source=redis-conf,target=/etc/redis/redis.conf,mode=0400 redis:3.0.6 + --config source=redis-conf,target=/etc/redis/redis.conf,mode=0400 redis:7.4.1 ``` To grant a service access to multiple configs, use multiple `--config` flags. @@ -239,7 +239,7 @@ $ docker service create \ --name redis \ --update-delay 10s \ --update-parallelism 2 \ - redis:3.0.6 + redis:7.4.1 ``` When you run a [service update](service_update.md), the scheduler updates a @@ -256,7 +256,7 @@ $ docker service create \ --name redis_2 \ --replicas 5 \ --env MYVAR=foo \ - redis:3.0.6 + redis:7.4.1 ``` To specify multiple environment variables, specify multiple `--env` flags, each @@ -268,7 +268,7 @@ $ docker service create \ --replicas 5 \ --env MYVAR=foo \ --env MYVAR2=bar \ - redis:3.0.6 + redis:7.4.1 ``` ### Create a service with specific hostname (--hostname) @@ -277,7 +277,7 @@ This option sets the docker service containers hostname to a specific string. For example: ```console -$ docker service create --name redis --hostname myredis redis:3.0.6 +$ docker service create --name redis --hostname myredis redis:7.4.1 ``` ### Set metadata on a service (-l, --label) @@ -290,7 +290,7 @@ $ docker service create \ --name redis_2 \ --label com.example.foo="bar" \ --label bar=baz \ - redis:3.0.6 + redis:7.4.1 ``` For more information about labels, refer to [apply custom @@ -679,7 +679,7 @@ The following command creates a global service: $ docker service create \ --name redis_2 \ --mode global \ - redis:3.0.6 + redis:7.4.1 ``` ### Specify service constraints (--constraint) @@ -698,7 +698,7 @@ follows: | `node.platform.os` | Node operating system | `node.platform.os==windows` | | `node.platform.arch` | Node architecture | `node.platform.arch==x86_64` | | `node.labels` | User-defined node labels | `node.labels.security==high` | -| `engine.labels` | Docker Engine's labels | `engine.labels.operatingsystem==ubuntu-22.04` | +| `engine.labels` | Docker Engine's labels | `engine.labels.operatingsystem==ubuntu-24.04` | `engine.labels` apply to Docker Engine labels like operating system, drivers, etc. Swarm administrators add `node.labels` for operational purposes by using @@ -712,7 +712,7 @@ $ docker service create \ --name redis_2 \ --constraint node.platform.os==linux \ --constraint node.labels.type==queue \ - redis:3.0.6 + redis:7.4.1 ``` If the service constraints exclude all nodes in the cluster, a message is printed @@ -760,7 +760,7 @@ $ docker service create \ --replicas 9 \ --name redis_2 \ --placement-pref spread=node.labels.datacenter \ - redis:3.0.6 + redis:7.4.1 ``` This uses `--placement-pref` with a `spread` strategy (currently the only @@ -812,7 +812,7 @@ $ docker service create \ --name redis_2 \ --placement-pref 'spread=node.labels.datacenter' \ --placement-pref 'spread=node.labels.rack' \ - redis:3.0.6 + redis:7.4.1 ``` When updating a service with `docker service update`, `--placement-pref-add` @@ -940,7 +940,7 @@ $ docker service create \ The swarm extends my-network to each node running the service. Containers on the same network can access each other using -[service discovery](https://docs.docker.com/network/drivers/overlay/#container-discovery). +[service discovery](https://docs.docker.com/engine/network/drivers/overlay/#container-discovery). Long form syntax of `--network` allows to specify list of aliases and driver options: `--network name=my-network,alias=web1,driver-opt=field1=value1` diff --git a/docs/reference/commandline/service_inspect.md b/docs/reference/commandline/service_inspect.md index 9bfd6f0afdab..2d81e98a702b 100644 --- a/docs/reference/commandline/service_inspect.md +++ b/docs/reference/commandline/service_inspect.md @@ -8,7 +8,7 @@ Display detailed information on one or more services | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`-f`](#format), [`--format`](#format) | `string` | | Format output using a custom template:
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| [`--pretty`](#pretty) | | | Print the information in a human friendly format | +| [`--pretty`](#pretty) | `bool` | | Print the information in a human friendly format | @@ -23,8 +23,7 @@ the given template will be executed for each result. Go's [text/template](https://pkg.go.dev/text/template) package describes all the details of the format. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the @@ -41,7 +40,7 @@ For example, given the following service; ```console $ docker service ls ID NAME MODE REPLICAS IMAGE -dmu1ept4cxcf redis replicated 3/3 redis:3.0.6 +dmu1ept4cxcf redis replicated 3/3 redis:7.4.1 ``` Both `docker service inspect redis`, and `docker service inspect dmu1ept4cxcf` @@ -66,7 +65,7 @@ The output is in JSON format, for example: "Name": "redis", "TaskTemplate": { "ContainerSpec": { - "Image": "redis:3.0.6" + "Image": "redis:7.4.1" }, "Resources": { "Limits": {}, diff --git a/docs/reference/commandline/service_logs.md b/docs/reference/commandline/service_logs.md index 43919400ffe5..fa8907bbf53c 100644 --- a/docs/reference/commandline/service_logs.md +++ b/docs/reference/commandline/service_logs.md @@ -7,15 +7,15 @@ Fetch the logs of a service or task | Name | Type | Default | Description | |:---------------------|:---------|:--------|:------------------------------------------------------------------------------------------------| -| `--details` | | | Show extra details provided to logs | -| `-f`, `--follow` | | | Follow log output | -| `--no-resolve` | | | Do not map IDs to Names in output | -| `--no-task-ids` | | | Do not include task IDs in output | -| `--no-trunc` | | | Do not truncate output | -| `--raw` | | | Do not neatly format logs | +| `--details` | `bool` | | Show extra details provided to logs | +| `-f`, `--follow` | `bool` | | Follow log output | +| `--no-resolve` | `bool` | | Do not map IDs to Names in output | +| `--no-task-ids` | `bool` | | Do not include task IDs in output | +| `--no-trunc` | `bool` | | Do not truncate output | +| `--raw` | `bool` | | Do not neatly format logs | | `--since` | `string` | | Show logs since timestamp (e.g. `2013-01-02T13:23:37Z`) or relative (e.g. `42m` for 42 minutes) | | `-n`, `--tail` | `string` | `all` | Number of lines to show from the end of the logs | -| `-t`, `--timestamps` | | | Show timestamps | +| `-t`, `--timestamps` | `bool` | | Show timestamps | @@ -24,8 +24,7 @@ Fetch the logs of a service or task The `docker service logs` command batch-retrieves logs present at the time of execution. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the @@ -36,13 +35,12 @@ service, or with the ID of a task. If a service is passed, it will display logs for all of the containers in that service. If a task is passed, it will only display logs from that particular task. -> **Note** -> +> [!NOTE] > This command is only functional for services that are started with > the `json-file` or `journald` logging driver. For more information about selecting and configuring logging drivers, refer to -[Configure logging drivers](https://docs.docker.com/config/containers/logging/configure/). +[Configure logging drivers](https://docs.docker.com/engine/logging/configure/). The `docker service logs --follow` command will continue streaming the new output from the service's `STDOUT` and `STDERR`. diff --git a/docs/reference/commandline/service_ls.md b/docs/reference/commandline/service_ls.md index 304568f840c2..9cb4d08d84c6 100644 --- a/docs/reference/commandline/service_ls.md +++ b/docs/reference/commandline/service_ls.md @@ -13,7 +13,7 @@ List services |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `-q`, `--quiet` | | | Only display IDs | +| `-q`, `--quiet` | `bool` | | Only display IDs | @@ -22,8 +22,7 @@ List services This command lists services that are running in the swarm. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the @@ -38,7 +37,7 @@ $ docker service ls ID NAME MODE REPLICAS IMAGE c8wgl7q4ndfd frontend replicated 5/5 nginx:alpine -dmu1ept4cxcf redis replicated 3/3 redis:3.0.6 +dmu1ept4cxcf redis replicated 3/3 redis:7.4.1 iwe3278osahj mongo global 7/7 mongo:3.3 hh08h9uu8uwr job replicated-job 1/1 (3/5 completed) nginx:latest ``` @@ -69,7 +68,7 @@ The following filter matches services with an ID starting with `0bcjw`: ```console $ docker service ls -f "id=0bcjw" ID NAME MODE REPLICAS IMAGE -0bcjwfh8ychr redis replicated 1/1 redis:3.0.6 +0bcjwfh8ychr redis replicated 1/1 redis:7.4.1 ``` #### label @@ -85,7 +84,7 @@ $ docker service ls --filter label=project ID NAME MODE REPLICAS IMAGE 01sl1rp6nj5u frontend2 replicated 1/1 nginx:alpine 36xvvwwauej0 frontend replicated 5/5 nginx:alpine -74nzcxxjv6fq backend replicated 3/3 redis:3.0.6 +74nzcxxjv6fq backend replicated 3/3 redis:7.4.1 ``` The following filter matches only services with the `project` label with the @@ -95,7 +94,7 @@ The following filter matches only services with the `project` label with the $ docker service ls --filter label=project=project-a ID NAME MODE REPLICAS IMAGE 36xvvwwauej0 frontend replicated 5/5 nginx:alpine -74nzcxxjv6fq backend replicated 3/3 redis:3.0.6 +74nzcxxjv6fq backend replicated 3/3 redis:7.4.1 ``` #### mode @@ -119,7 +118,7 @@ The following filter matches services with a name starting with `redis`. ```console $ docker service ls --filter name=redis ID NAME MODE REPLICAS IMAGE -0bcjwfh8ychr redis replicated 1/1 redis:3.0.6 +0bcjwfh8ychr redis replicated 1/1 redis:7.4.1 ``` ### Format the output (--format) diff --git a/docs/reference/commandline/service_ps.md b/docs/reference/commandline/service_ps.md index 9736315d71e6..0cd1d4360c7b 100644 --- a/docs/reference/commandline/service_ps.md +++ b/docs/reference/commandline/service_ps.md @@ -9,9 +9,9 @@ List the tasks of one or more services |:---------------------------------------|:---------|:--------|:-------------------------------------------| | [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Pretty-print tasks using a Go template | -| `--no-resolve` | | | Do not map IDs to Names | -| `--no-trunc` | | | Do not truncate output | -| `-q`, `--quiet` | | | Only display task IDs | +| `--no-resolve` | `bool` | | Do not map IDs to Names | +| `--no-trunc` | `bool` | | Do not truncate output | +| `-q`, `--quiet` | `bool` | | Only display task IDs | @@ -20,8 +20,7 @@ List the tasks of one or more services Lists the tasks that are running as part of the specified services. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the @@ -37,35 +36,35 @@ The following command shows all the tasks that are part of the `redis` service: $ docker service ps redis ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS -0qihejybwf1x redis.1 redis:3.0.5 manager1 Running Running 8 seconds -bk658fpbex0d redis.2 redis:3.0.5 worker2 Running Running 9 seconds -5ls5s5fldaqg redis.3 redis:3.0.5 worker1 Running Running 9 seconds -8ryt076polmc redis.4 redis:3.0.5 worker1 Running Running 9 seconds -1x0v8yomsncd redis.5 redis:3.0.5 manager1 Running Running 8 seconds -71v7je3el7rr redis.6 redis:3.0.5 worker2 Running Running 9 seconds -4l3zm9b7tfr7 redis.7 redis:3.0.5 worker2 Running Running 9 seconds -9tfpyixiy2i7 redis.8 redis:3.0.5 worker1 Running Running 9 seconds -3w1wu13yupln redis.9 redis:3.0.5 manager1 Running Running 8 seconds -8eaxrb2fqpbn redis.10 redis:3.0.5 manager1 Running Running 8 seconds +0qihejybwf1x redis.1 redis:7.4.0 manager1 Running Running 8 seconds +bk658fpbex0d redis.2 redis:7.4.0 worker2 Running Running 9 seconds +5ls5s5fldaqg redis.3 redis:7.4.0 worker1 Running Running 9 seconds +8ryt076polmc redis.4 redis:7.4.0 worker1 Running Running 9 seconds +1x0v8yomsncd redis.5 redis:7.4.0 manager1 Running Running 8 seconds +71v7je3el7rr redis.6 redis:7.4.0 worker2 Running Running 9 seconds +4l3zm9b7tfr7 redis.7 redis:7.4.0 worker2 Running Running 9 seconds +9tfpyixiy2i7 redis.8 redis:7.4.0 worker1 Running Running 9 seconds +3w1wu13yupln redis.9 redis:7.4.0 manager1 Running Running 8 seconds +8eaxrb2fqpbn redis.10 redis:7.4.0 manager1 Running Running 8 seconds ``` In addition to running tasks, the output also shows the task history. For -example, after updating the service to use the `redis:3.0.6` image, the output +example, after updating the service to use the `redis:7.4.1` image, the output may look like this: ```console $ docker service ps redis ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS -50qe8lfnxaxk redis.1 redis:3.0.6 manager1 Running Running 6 seconds ago -ky2re9oz86r9 \_ redis.1 redis:3.0.5 manager1 Shutdown Shutdown 8 seconds ago -3s46te2nzl4i redis.2 redis:3.0.6 worker2 Running Running less than a second ago -nvjljf7rmor4 \_ redis.2 redis:3.0.6 worker2 Shutdown Rejected 23 seconds ago "No such image: redis@sha256:6…" -vtiuz2fpc0yb \_ redis.2 redis:3.0.5 worker2 Shutdown Shutdown 1 second ago -jnarweeha8x4 redis.3 redis:3.0.6 worker1 Running Running 3 seconds ago -vs448yca2nz4 \_ redis.3 redis:3.0.5 worker1 Shutdown Shutdown 4 seconds ago -jf1i992619ir redis.4 redis:3.0.6 worker1 Running Running 10 seconds ago -blkttv7zs8ee \_ redis.4 redis:3.0.5 worker1 Shutdown Shutdown 11 seconds ago +50qe8lfnxaxk redis.1 redis:7.4.1 manager1 Running Running 6 seconds ago +ky2re9oz86r9 \_ redis.1 redis:7.4.0 manager1 Shutdown Shutdown 8 seconds ago +3s46te2nzl4i redis.2 redis:7.4.1 worker2 Running Running less than a second ago +nvjljf7rmor4 \_ redis.2 redis:7.4.1 worker2 Shutdown Rejected 23 seconds ago "No such image: redis@sha256:6…" +vtiuz2fpc0yb \_ redis.2 redis:7.4.0 worker2 Shutdown Shutdown 1 second ago +jnarweeha8x4 redis.3 redis:7.4.1 worker1 Running Running 3 seconds ago +vs448yca2nz4 \_ redis.3 redis:7.4.0 worker1 Shutdown Shutdown 4 seconds ago +jf1i992619ir redis.4 redis:7.4.1 worker1 Running Running 10 seconds ago +blkttv7zs8ee \_ redis.4 redis:7.4.0 worker1 Shutdown Shutdown 11 seconds ago ``` The number of items in the task history is determined by the @@ -83,10 +82,10 @@ example: $ docker service ps --no-trunc redis ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS -50qe8lfnxaxksi9w2a704wkp7 redis.1 redis:3.0.6@sha256:6a692a76c2081888b589e26e6ec835743119fe453d67ecf03df7de5b73d69842 manager1 Running Running 5 minutes ago -ky2re9oz86r9556i2szb8a8af \_ redis.1 redis:3.0.5@sha256:f8829e00d95672c48c60f468329d6693c4bdd28d1f057e755f8ba8b40008682e worker2 Shutdown Shutdown 5 minutes ago -bk658fpbex0d57cqcwoe3jthu redis.2 redis:3.0.6@sha256:6a692a76c2081888b589e26e6ec835743119fe453d67ecf03df7de5b73d69842 worker2 Running Running 5 seconds -nvjljf7rmor4htv7l8rwcx7i7 \_ redis.2 redis:3.0.6@sha256:6a692a76c2081888b589e26e6ec835743119fe453d67ecf03df7de5b73d69842 worker2 Shutdown Rejected 5 minutes ago "No such image: redis@sha256:6a692a76c2081888b589e26e6ec835743119fe453d67ecf03df7de5b73d69842" +50qe8lfnxaxksi9w2a704wkp7 redis.1 redis:7.4.1@sha256:6a692a76c2081888b589e26e6ec835743119fe453d67ecf03df7de5b73d69842 manager1 Running Running 5 minutes ago +ky2re9oz86r9556i2szb8a8af \_ redis.1 redis:7.4.0@sha256:f8829e00d95672c48c60f468329d6693c4bdd28d1f057e755f8ba8b40008682e worker2 Shutdown Shutdown 5 minutes ago +bk658fpbex0d57cqcwoe3jthu redis.2 redis:7.4.1@sha256:6a692a76c2081888b589e26e6ec835743119fe453d67ecf03df7de5b73d69842 worker2 Running Running 5 seconds +nvjljf7rmor4htv7l8rwcx7i7 \_ redis.2 redis:7.4.1@sha256:6a692a76c2081888b589e26e6ec835743119fe453d67ecf03df7de5b73d69842 worker2 Shutdown Rejected 5 minutes ago "No such image: redis@sha256:6a692a76c2081888b589e26e6ec835743119fe453d67ecf03df7de5b73d69842" ``` ### Filtering (--filter) @@ -112,8 +111,8 @@ The `id` filter matches on all or a prefix of a task's ID. $ docker service ps -f "id=8" redis ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS -8ryt076polmc redis.4 redis:3.0.6 worker1 Running Running 9 seconds -8eaxrb2fqpbn redis.10 redis:3.0.6 manager1 Running Running 8 seconds +8ryt076polmc redis.4 redis:7.4.1 worker1 Running Running 9 seconds +8eaxrb2fqpbn redis.10 redis:7.4.1 manager1 Running Running 8 seconds ``` #### name @@ -124,7 +123,7 @@ The `name` filter matches on task names. $ docker service ps -f "name=redis.1" redis ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS -qihejybwf1x5 redis.1 redis:3.0.6 manager1 Running Running 8 seconds +qihejybwf1x5 redis.1 redis:7.4.1 manager1 Running Running 8 seconds ``` @@ -136,10 +135,10 @@ The `node` filter matches on a node name or a node ID. $ docker service ps -f "node=manager1" redis ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS -0qihejybwf1x redis.1 redis:3.0.6 manager1 Running Running 8 seconds -1x0v8yomsncd redis.5 redis:3.0.6 manager1 Running Running 8 seconds -3w1wu13yupln redis.9 redis:3.0.6 manager1 Running Running 8 seconds -8eaxrb2fqpbn redis.10 redis:3.0.6 manager1 Running Running 8 seconds +0qihejybwf1x redis.1 redis:7.4.1 manager1 Running Running 8 seconds +1x0v8yomsncd redis.5 redis:7.4.1 manager1 Running Running 8 seconds +3w1wu13yupln redis.9 redis:7.4.1 manager1 Running Running 8 seconds +8eaxrb2fqpbn redis.10 redis:7.4.1 manager1 Running Running 8 seconds ``` #### desired-state diff --git a/docs/reference/commandline/service_rm.md b/docs/reference/commandline/service_rm.md index e974bb90f48a..586ea1983306 100644 --- a/docs/reference/commandline/service_rm.md +++ b/docs/reference/commandline/service_rm.md @@ -14,8 +14,7 @@ Remove one or more services Removes the specified services from the swarm. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the @@ -35,8 +34,7 @@ $ docker service ls ID NAME MODE REPLICAS IMAGE ``` -> **Warning** -> +> [!WARNING] > Unlike `docker rm`, this command does not ask for confirmation before removing > a running service. diff --git a/docs/reference/commandline/service_rollback.md b/docs/reference/commandline/service_rollback.md index 52aaf1ce3969..f7a68870e97d 100644 --- a/docs/reference/commandline/service_rollback.md +++ b/docs/reference/commandline/service_rollback.md @@ -5,10 +5,10 @@ Revert changes to a service's configuration ### Options -| Name | Type | Default | Description | -|:-----------------|:-----|:--------|:----------------------------------------------------------------| -| `-d`, `--detach` | | | Exit immediately instead of waiting for the service to converge | -| `-q`, `--quiet` | | | Suppress progress output | +| Name | Type | Default | Description | +|:-----------------|:-------|:--------|:----------------------------------------------------------------| +| `-d`, `--detach` | `bool` | | Exit immediately instead of waiting for the service to converge | +| `-q`, `--quiet` | `bool` | | Suppress progress output | @@ -17,8 +17,7 @@ Revert changes to a service's configuration Roll back a specified service to its previous version from the swarm. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/service_scale.md b/docs/reference/commandline/service_scale.md index 594e6aea9b90..cc9494f57d17 100644 --- a/docs/reference/commandline/service_scale.md +++ b/docs/reference/commandline/service_scale.md @@ -5,9 +5,9 @@ Scale one or multiple replicated services ### Options -| Name | Type | Default | Description | -|:-----------------|:-----|:--------|:----------------------------------------------------------------| -| `-d`, `--detach` | | | Exit immediately instead of waiting for the service to converge | +| Name | Type | Default | Description | +|:-----------------|:-------|:--------|:----------------------------------------------------------------| +| `-d`, `--detach` | `bool` | | Exit immediately instead of waiting for the service to converge | @@ -20,8 +20,7 @@ services which are global mode. The command will return immediately, but the actual scaling of the service may take some time. To stop all replicas of a service while keeping the service active in the swarm you can set the scale to 0. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the @@ -85,7 +84,7 @@ $ docker service ls ID NAME MODE REPLICAS IMAGE 3pr5mlvu3fh9 frontend replicated 5/5 nginx:alpine -74nzcxxjv6fq backend replicated 3/3 redis:3.0.6 +74nzcxxjv6fq backend replicated 3/3 redis:7.4.1 ``` ## Related commands diff --git a/docs/reference/commandline/service_update.md b/docs/reference/commandline/service_update.md index 3c9d4ee96091..f3564bcc1f24 100644 --- a/docs/reference/commandline/service_update.md +++ b/docs/reference/commandline/service_update.md @@ -17,7 +17,7 @@ Update a service | `--container-label-add` | `list` | | Add or update a container label | | `--container-label-rm` | `list` | | Remove a container label by its key | | `--credential-spec` | `credential-spec` | | Credential spec for managed service account (Windows only) | -| `-d`, `--detach` | | | Exit immediately instead of waiting for the service to converge | +| `-d`, `--detach` | `bool` | | Exit immediately instead of waiting for the service to converge | | `--dns-add` | `list` | | Add or update a custom DNS server | | `--dns-option-add` | `list` | | Add or update a DNS option | | `--dns-option-rm` | `list` | | Remove a DNS option | @@ -28,7 +28,7 @@ Update a service | `--entrypoint` | `command` | | Overwrite the default ENTRYPOINT of the image | | `--env-add` | `list` | | Add or update an environment variable | | `--env-rm` | `list` | | Remove an environment variable | -| `--force` | | | Force update even if no changes require it | +| `--force` | `bool` | | Force update even if no changes require it | | `--generic-resource-add` | `list` | | Add a Generic resource | | `--generic-resource-rm` | `list` | | Remove a Generic resource | | `--group-add` | `list` | | Add an additional supplementary user group to the container | @@ -43,7 +43,7 @@ Update a service | `--host-rm` | `list` | | Remove a custom host-to-IP mapping (`host:ip`) | | `--hostname` | `string` | | Container hostname | | `--image` | `string` | | Service image tag | -| `--init` | | | Use an init inside each service container to forward signals and reap processes | +| `--init` | `bool` | | Use an init inside each service container to forward signals and reap processes | | [`--isolation`](#isolation) | `string` | | Service container isolation mode | | `--label-add` | `list` | | Add or update a service label | | `--label-rm` | `list` | | Remove a label by its key | @@ -57,14 +57,15 @@ Update a service | `--mount-rm` | `list` | | Remove a mount by its target path | | [`--network-add`](#network-add) | `network` | | Add a network | | `--network-rm` | `list` | | Remove a network | -| `--no-healthcheck` | | | Disable any container-specified HEALTHCHECK | -| `--no-resolve-image` | | | Do not query the registry to resolve image digest and supported platforms | +| `--no-healthcheck` | `bool` | | Disable any container-specified HEALTHCHECK | +| `--no-resolve-image` | `bool` | | Do not query the registry to resolve image digest and supported platforms | +| `--oom-score-adj` | `int64` | `0` | Tune host's OOM preferences (-1000 to 1000) | | `--placement-pref-add` | `pref` | | Add a placement preference | | `--placement-pref-rm` | `pref` | | Remove a placement preference | | [`--publish-add`](#publish-add) | `port` | | Add or update a published port | | `--publish-rm` | `port` | | Remove a published port by its target port | -| `-q`, `--quiet` | | | Suppress progress output | -| `--read-only` | | | Mount the container's root filesystem as read only | +| `-q`, `--quiet` | `bool` | | Suppress progress output | +| `--read-only` | `bool` | | Mount the container's root filesystem as read only | | `--replicas` | `uint` | | Number of tasks | | `--replicas-max-per-node` | `uint64` | `0` | Maximum number of tasks per node (default 0 = unlimited) | | `--reserve-cpu` | `decimal` | | Reserve CPUs | @@ -73,7 +74,7 @@ Update a service | `--restart-delay` | `duration` | | Delay between restart attempts (ns\|us\|ms\|s\|m\|h) | | `--restart-max-attempts` | `uint` | | Maximum number of restarts before giving up | | `--restart-window` | `duration` | | Window used to evaluate the restart policy (ns\|us\|ms\|s\|m\|h) | -| [`--rollback`](#rollback) | | | Rollback to previous specification | +| [`--rollback`](#rollback) | `bool` | | Rollback to previous specification | | `--rollback-delay` | `duration` | `0s` | Delay between task rollbacks (ns\|us\|ms\|s\|m\|h) | | `--rollback-failure-action` | `string` | | Action on rollback failure (`pause`, `continue`) | | `--rollback-max-failure-ratio` | `float` | `0` | Failure rate to tolerate during a rollback | @@ -86,7 +87,7 @@ Update a service | `--stop-signal` | `string` | | Signal to stop the container | | `--sysctl-add` | `list` | | Add or update a Sysctl option | | `--sysctl-rm` | `list` | | Remove a Sysctl option | -| `-t`, `--tty` | | | Allocate a pseudo-TTY | +| `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY | | `--ulimit-add` | `ulimit` | | Add or update a ulimit option | | `--ulimit-rm` | `list` | | Remove a ulimit option | | `--update-delay` | `duration` | `0s` | Delay between updates (ns\|us\|ms\|s\|m\|h) | @@ -96,7 +97,7 @@ Update a service | `--update-order` | `string` | | Update order (`start-first`, `stop-first`) | | [`--update-parallelism`](#update-parallelism) | `uint64` | `0` | Maximum number of tasks updated simultaneously (0 to update all at once) | | `-u`, `--user` | `string` | | Username or UID (format: [:]) | -| `--with-registry-auth` | | | Send registry authentication details to swarm agents | +| `--with-registry-auth` | `bool` | | Send registry authentication details to swarm agents | | `-w`, `--workdir` | `string` | | Working directory inside the container | @@ -114,8 +115,7 @@ service requires recreating the tasks for it to take effect. For example, only c setting. However, the `--force` flag will cause the tasks to be recreated anyway. This can be used to perform a rolling restart without any changes to the service parameters. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/stack_config.md b/docs/reference/commandline/stack_config.md index 411d6d61527b..0ad91cb8137c 100644 --- a/docs/reference/commandline/stack_config.md +++ b/docs/reference/commandline/stack_config.md @@ -8,7 +8,7 @@ Outputs the final config file, after doing merges and interpolations | Name | Type | Default | Description | |:-----------------------|:--------------|:--------|:--------------------------------------------------| | `-c`, `--compose-file` | `stringSlice` | | Path to a Compose file, or `-` to read from stdin | -| `--skip-interpolation` | | | Skip interpolation and output only merged config | +| `--skip-interpolation` | `bool` | | Skip interpolation and output only merged config | diff --git a/docs/reference/commandline/stack_deploy.md b/docs/reference/commandline/stack_deploy.md index 82934f6f9221..01ccf278875f 100644 --- a/docs/reference/commandline/stack_deploy.md +++ b/docs/reference/commandline/stack_deploy.md @@ -13,10 +13,10 @@ Deploy a new stack or update an existing stack |:---------------------------------------------------------|:--------------|:---------|:--------------------------------------------------------------------------------------------------| | [`-c`](#compose-file), [`--compose-file`](#compose-file) | `stringSlice` | | Path to a Compose file, or `-` to read from stdin | | `-d`, `--detach` | `bool` | `true` | Exit immediately instead of waiting for the stack services to converge | -| `--prune` | | | Prune services that are no longer referenced | -| `-q`, `--quiet` | | | Suppress progress output | +| `--prune` | `bool` | | Prune services that are no longer referenced | +| `-q`, `--quiet` | `bool` | | Suppress progress output | | `--resolve-image` | `string` | `always` | Query the registry to resolve image digest and supported platforms (`always`, `changed`, `never`) | -| `--with-registry-auth` | | | Send registry authentication details to Swarm agents | +| `--with-registry-auth` | `bool` | | Send registry authentication details to Swarm agents | @@ -25,8 +25,7 @@ Deploy a new stack or update an existing stack Create and update a stack from a `compose` file on the swarm. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/stack_ls.md b/docs/reference/commandline/stack_ls.md index 075662a6743f..bbcc27fe5231 100644 --- a/docs/reference/commandline/stack_ls.md +++ b/docs/reference/commandline/stack_ls.md @@ -20,8 +20,7 @@ List stacks Lists the stacks. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/stack_ps.md b/docs/reference/commandline/stack_ps.md index b9d2c4798d17..1fa10a286168 100644 --- a/docs/reference/commandline/stack_ps.md +++ b/docs/reference/commandline/stack_ps.md @@ -9,9 +9,9 @@ List the tasks in the stack |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| [`--no-resolve`](#no-resolve) | | | Do not map IDs to Names | -| [`--no-trunc`](#no-trunc) | | | Do not truncate output | -| [`-q`](#quiet), [`--quiet`](#quiet) | | | Only display task IDs | +| [`--no-resolve`](#no-resolve) | `bool` | | Do not map IDs to Names | +| [`--no-trunc`](#no-trunc) | `bool` | | Do not truncate output | +| [`-q`](#quiet), [`--quiet`](#quiet) | `bool` | | Only display task IDs | @@ -20,8 +20,7 @@ List the tasks in the stack Lists the tasks that are running as part of the specified stack. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/stack_rm.md b/docs/reference/commandline/stack_rm.md index 5cfbfaab1bbc..aa82c72b2df0 100644 --- a/docs/reference/commandline/stack_rm.md +++ b/docs/reference/commandline/stack_rm.md @@ -20,8 +20,7 @@ Remove one or more stacks Remove the stack from the swarm. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/stack_services.md b/docs/reference/commandline/stack_services.md index 251a6ce336db..e940207b051f 100644 --- a/docs/reference/commandline/stack_services.md +++ b/docs/reference/commandline/stack_services.md @@ -9,7 +9,7 @@ List the services in the stack |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `-q`, `--quiet` | | | Only display IDs | +| `-q`, `--quiet` | `bool` | | Only display IDs | @@ -18,8 +18,7 @@ List the services in the stack Lists the services that are running as part of the specified stack. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/start.md b/docs/reference/commandline/start.md index d4ea72a7c112..b89b47deea73 100644 --- a/docs/reference/commandline/start.md +++ b/docs/reference/commandline/start.md @@ -11,11 +11,11 @@ Start one or more stopped containers | Name | Type | Default | Description | |:----------------------|:---------|:--------|:----------------------------------------------------| -| `-a`, `--attach` | | | Attach STDOUT/STDERR and forward signals | +| `-a`, `--attach` | `bool` | | Attach STDOUT/STDERR and forward signals | | `--checkpoint` | `string` | | Restore from this checkpoint | | `--checkpoint-dir` | `string` | | Use a custom checkpoint storage directory | | `--detach-keys` | `string` | | Override the key sequence for detaching a container | -| `-i`, `--interactive` | | | Attach container's STDIN | +| `-i`, `--interactive` | `bool` | | Attach container's STDIN | diff --git a/docs/reference/commandline/stats.md b/docs/reference/commandline/stats.md index f1fec24e4941..efb8cd129155 100644 --- a/docs/reference/commandline/stats.md +++ b/docs/reference/commandline/stats.md @@ -11,10 +11,10 @@ Display a live stream of container(s) resource usage statistics | Name | Type | Default | Description | |:--------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `-a`, `--all` | | | Show all containers (default shows just running) | +| `-a`, `--all` | `bool` | | Show all containers (default shows just running) | | `--format` | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `--no-stream` | | | Disable streaming stats and only pull the first result | -| `--no-trunc` | | | Do not truncate output | +| `--no-stream` | `bool` | | Disable streaming stats and only pull the first result | +| `--no-trunc` | `bool` | | Do not truncate output | diff --git a/docs/reference/commandline/swarm_ca.md b/docs/reference/commandline/swarm_ca.md index 28861091732f..49c2730aa9a7 100644 --- a/docs/reference/commandline/swarm_ca.md +++ b/docs/reference/commandline/swarm_ca.md @@ -10,10 +10,10 @@ Display and rotate the root CA | `--ca-cert` | `pem-file` | | Path to the PEM-formatted root CA certificate to use for the new cluster | | `--ca-key` | `pem-file` | | Path to the PEM-formatted root CA key to use for the new cluster | | `--cert-expiry` | `duration` | `2160h0m0s` | Validity period for node certificates (ns\|us\|ms\|s\|m\|h) | -| [`-d`](#detach), [`--detach`](#detach) | | | Exit immediately instead of waiting for the root rotation to converge | +| [`-d`](#detach), [`--detach`](#detach) | `bool` | | Exit immediately instead of waiting for the root rotation to converge | | `--external-ca` | `external-ca` | | Specifications of one or more certificate signing endpoints | -| `-q`, `--quiet` | | | Suppress progress output | -| [`--rotate`](#rotate) | | | Rotate the swarm CA - if no certificate or key are provided, new ones will be generated | +| `-q`, `--quiet` | `bool` | | Suppress progress output | +| [`--rotate`](#rotate) | `bool` | | Rotate the swarm CA - if no certificate or key are provided, new ones will be generated | @@ -22,8 +22,7 @@ Display and rotate the root CA View or rotate the current swarm CA certificate. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the @@ -81,8 +80,7 @@ gyg5u9Iliel99l7SuMhNeLkrU7fXs+Of1nTyyM73ig== ### Root CA rotation (--rotate) -> **Note** -> +> [!NOTE] > Mirantis Kubernetes Engine (MKE), formerly known as Docker UCP, provides an external > certificate manager service for the swarm. If you run swarm on MKE, you shouldn't > rotate the CA certificates manually. Instead, contact Mirantis support if you need diff --git a/docs/reference/commandline/swarm_init.md b/docs/reference/commandline/swarm_init.md index 29897e8d0379..c189a98eedf7 100644 --- a/docs/reference/commandline/swarm_init.md +++ b/docs/reference/commandline/swarm_init.md @@ -8,7 +8,7 @@ Initialize a swarm | Name | Type | Default | Description | |:--------------------------------------------|:--------------|:---------------|:-----------------------------------------------------------------------------------------------------------------------------| | [`--advertise-addr`](#advertise-addr) | `string` | | Advertised address (format: `[:port]`) | -| [`--autolock`](#autolock) | | | Enable manager autolocking (requiring an unlock key to start a stopped manager) | +| [`--autolock`](#autolock) | `bool` | | Enable manager autolocking (requiring an unlock key to start a stopped manager) | | [`--availability`](#availability) | `string` | `active` | Availability of the node (`active`, `pause`, `drain`) | | `--cert-expiry` | `duration` | `2160h0m0s` | Validity period for node certificates (ns\|us\|ms\|s\|m\|h) | | [`--data-path-addr`](#data-path-addr) | `string` | | Address or interface to use for data path traffic (format: ``) | @@ -17,7 +17,7 @@ Initialize a swarm | `--default-addr-pool-mask-length` | `uint32` | `24` | default address pool subnet mask length | | `--dispatcher-heartbeat` | `duration` | `5s` | Dispatcher heartbeat period (ns\|us\|ms\|s\|m\|h) | | [`--external-ca`](#external-ca) | `external-ca` | | Specifications of one or more certificate signing endpoints | -| [`--force-new-cluster`](#force-new-cluster) | | | Force create a new cluster from current state | +| [`--force-new-cluster`](#force-new-cluster) | `bool` | | Force create a new cluster from current state | | [`--listen-addr`](#listen-addr) | `node-addr` | `0.0.0.0:2377` | Listen address (format: `[:port]`) | | [`--max-snapshots`](#max-snapshots) | `uint64` | `0` | Number of additional Raft snapshots to retain | | [`--snapshot-interval`](#snapshot-interval) | `uint64` | `10000` | Number of log entries between Raft snapshots | diff --git a/docs/reference/commandline/swarm_join-token.md b/docs/reference/commandline/swarm_join-token.md index 405209f9e1ec..745d752ce4e0 100644 --- a/docs/reference/commandline/swarm_join-token.md +++ b/docs/reference/commandline/swarm_join-token.md @@ -5,10 +5,10 @@ Manage join tokens ### Options -| Name | Type | Default | Description | -|:----------------|:-----|:--------|:-------------------| -| `-q`, `--quiet` | | | Only display token | -| `--rotate` | | | Rotate join token | +| Name | Type | Default | Description | +|:----------------|:-------|:--------|:-------------------| +| `-q`, `--quiet` | `bool` | | Only display token | +| `--rotate` | `bool` | | Rotate join token | @@ -21,8 +21,7 @@ role. You pass the token using the `--token` flag when you run [swarm join](swarm_join.md). Nodes use the join token only when they join the swarm. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/swarm_leave.md b/docs/reference/commandline/swarm_leave.md index 459daa3a0fb9..41fa8013d0e0 100644 --- a/docs/reference/commandline/swarm_leave.md +++ b/docs/reference/commandline/swarm_leave.md @@ -5,9 +5,9 @@ Leave the swarm ### Options -| Name | Type | Default | Description | -|:----------------|:-----|:--------|:------------------------------------------------------| -| `-f`, `--force` | | | Force this node to leave the swarm, ignoring warnings | +| Name | Type | Default | Description | +|:----------------|:-------|:--------|:------------------------------------------------------| +| `-f`, `--force` | `bool` | | Force this node to leave the swarm, ignoring warnings | diff --git a/docs/reference/commandline/swarm_unlock-key.md b/docs/reference/commandline/swarm_unlock-key.md index a2bbf72f9087..aaf6abb911e2 100644 --- a/docs/reference/commandline/swarm_unlock-key.md +++ b/docs/reference/commandline/swarm_unlock-key.md @@ -5,10 +5,10 @@ Manage the unlock key ### Options -| Name | Type | Default | Description | -|:----------------|:-----|:--------|:-------------------| -| `-q`, `--quiet` | | | Only display token | -| `--rotate` | | | Rotate unlock key | +| Name | Type | Default | Description | +|:----------------|:-------|:--------|:-------------------| +| `-q`, `--quiet` | `bool` | | Only display token | +| `--rotate` | `bool` | | Rotate unlock key | @@ -22,8 +22,7 @@ swarm. You can view or rotate the unlock key using `swarm unlock-key`. To view the key, run the `docker swarm unlock-key` command without any arguments: -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/swarm_unlock.md b/docs/reference/commandline/swarm_unlock.md index dc1296140e70..ebf9052bafc5 100644 --- a/docs/reference/commandline/swarm_unlock.md +++ b/docs/reference/commandline/swarm_unlock.md @@ -13,8 +13,7 @@ used to reactivate a manager after its Docker daemon restarts if the autolock setting is turned on. The unlock key is printed at the time when autolock is enabled, and is also available from the `docker swarm unlock-key` command. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/swarm_update.md b/docs/reference/commandline/swarm_update.md index 4879d2ee5074..abf1caf87aa3 100644 --- a/docs/reference/commandline/swarm_update.md +++ b/docs/reference/commandline/swarm_update.md @@ -7,7 +7,7 @@ Update the swarm | Name | Type | Default | Description | |:-------------------------|:--------------|:------------|:------------------------------------------------------------| -| `--autolock` | | | Change manager autolocking setting (true\|false) | +| `--autolock` | `bool` | | Change manager autolocking setting (true\|false) | | `--cert-expiry` | `duration` | `2160h0m0s` | Validity period for node certificates (ns\|us\|ms\|s\|m\|h) | | `--dispatcher-heartbeat` | `duration` | `5s` | Dispatcher heartbeat period (ns\|us\|ms\|s\|m\|h) | | `--external-ca` | `external-ca` | | Specifications of one or more certificate signing endpoints | @@ -22,8 +22,7 @@ Update the swarm Updates a swarm with new parameter values. -> **Note** -> +> [!NOTE] > This is a cluster management command, and must be executed on a swarm > manager node. To learn about managers and workers, refer to the > [Swarm mode section](https://docs.docker.com/engine/swarm/) in the diff --git a/docs/reference/commandline/system_df.md b/docs/reference/commandline/system_df.md index 98906152a706..1bb561b0a398 100644 --- a/docs/reference/commandline/system_df.md +++ b/docs/reference/commandline/system_df.md @@ -8,7 +8,7 @@ Show docker disk usage | Name | Type | Default | Description | |:----------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `-v`, `--verbose` | | | Show detailed information on space usage | +| `-v`, `--verbose` | `bool` | | Show detailed information on space usage | @@ -62,8 +62,7 @@ my-named-vol 0 * `UNIQUE SIZE` is the amount of space that's only used by a given image * `SIZE` is the virtual size of the image, it's the sum of `SHARED SIZE` and `UNIQUE SIZE` -> **Note** -> +> [!NOTE] > Network information isn't shown, because it doesn't consume disk space. ## Performance diff --git a/docs/reference/commandline/system_events.md b/docs/reference/commandline/system_events.md index 376f65f2eee8..2a5c82d5c015 100644 --- a/docs/reference/commandline/system_events.md +++ b/docs/reference/commandline/system_events.md @@ -144,7 +144,7 @@ Docker configs report the following events: #### Limit events by time (--since, --until) The `--since` and `--until` parameters can be Unix timestamps, date formatted -timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed +timestamps, or Go duration strings supported by [ParseDuration](https://pkg.go.dev/time#ParseDuration) (e.g. `10m`, `1h30m`) computed relative to the client machine’s time. If you do not provide the `--since` option, the command returns only new and/or live events. Supported formats for date formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`, @@ -339,10 +339,10 @@ $ docker events --filter 'type=network' $ docker events --filter 'container=container_1' --filter 'container=container_2' -2014-09-03T15:49:29.999999999Z07:00 container die 4386fb97867d (image=ubuntu:22.04) -2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:22.04) -2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (imager=redis:2.8) -2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image=redis:2.8) +2014-09-03T15:49:29.999999999Z07:00 container die 4386fb97867d (image=ubuntu:24.04) +2014-05-10T17:42:14.999999999Z07:00 container stop 4386fb97867d (image=ubuntu:24.04) +2014-05-10T17:42:14.999999999Z07:00 container die 7805c1d35632 (imager=redis:7.2) +2014-09-03T15:49:29.999999999Z07:00 container stop 7805c1d35632 (image=redis:7.2) $ docker events --filter 'type=volume' diff --git a/docs/reference/commandline/system_prune.md b/docs/reference/commandline/system_prune.md index 04b7ace8f762..0d39d50feb99 100644 --- a/docs/reference/commandline/system_prune.md +++ b/docs/reference/commandline/system_prune.md @@ -7,10 +7,10 @@ Remove unused data | Name | Type | Default | Description | |:----------------------|:---------|:--------|:---------------------------------------------------| -| `-a`, `--all` | | | Remove all unused images not just dangling ones | +| `-a`, `--all` | `bool` | | Remove all unused images not just dangling ones | | [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `label==`) | -| `-f`, `--force` | | | Do not prompt for confirmation | -| `--volumes` | | | Prune anonymous volumes | +| `-f`, `--force` | `bool` | | Do not prompt for confirmation | +| `--volumes` | `bool` | | Prune anonymous volumes | @@ -104,7 +104,7 @@ The currently supported filters are: * label (`label=`, `label==`, `label!=`, or `label!==`) - only remove containers, images, networks, and volumes with (or without, in case `label!=...` is used) the specified labels. The `until` filter can be Unix timestamps, date formatted -timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed +timestamps, or Go duration strings supported by [ParseDuration](https://pkg.go.dev/time#ParseDuration) (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. Supported formats for date formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`, `2006-01-02T15:04:05.999999999`, `2006-01-02T07:00`, and `2006-01-02`. The local diff --git a/docs/reference/commandline/trust_inspect.md b/docs/reference/commandline/trust_inspect.md index 5cb0ac0394b7..11c8ecf6d233 100644 --- a/docs/reference/commandline/trust_inspect.md +++ b/docs/reference/commandline/trust_inspect.md @@ -5,9 +5,9 @@ Return low-level information about keys and signatures ### Options -| Name | Type | Default | Description | -|:-----------|:-----|:--------|:-------------------------------------------------| -| `--pretty` | | | Print the information in a human friendly format | +| Name | Type | Default | Description | +|:-----------|:-------|:--------|:-------------------------------------------------| +| `--pretty` | `bool` | | Print the information in a human friendly format | diff --git a/docs/reference/commandline/trust_revoke.md b/docs/reference/commandline/trust_revoke.md index 06a2aa4eddc4..ed9efa52ab3d 100644 --- a/docs/reference/commandline/trust_revoke.md +++ b/docs/reference/commandline/trust_revoke.md @@ -5,9 +5,9 @@ Remove trust for an image ### Options -| Name | Type | Default | Description | -|:--------------|:-----|:--------|:-------------------------------| -| `-y`, `--yes` | | | Do not prompt for confirmation | +| Name | Type | Default | Description | +|:--------------|:-------|:--------|:-------------------------------| +| `-y`, `--yes` | `bool` | | Do not prompt for confirmation | diff --git a/docs/reference/commandline/trust_sign.md b/docs/reference/commandline/trust_sign.md index c5b8148224f8..c8596d571931 100644 --- a/docs/reference/commandline/trust_sign.md +++ b/docs/reference/commandline/trust_sign.md @@ -5,9 +5,9 @@ Sign an image ### Options -| Name | Type | Default | Description | -|:----------|:-----|:--------|:----------------------------| -| `--local` | | | Sign a locally tagged image | +| Name | Type | Default | Description | +|:----------|:-------|:--------|:----------------------------| +| `--local` | `bool` | | Sign a locally tagged image | diff --git a/docs/reference/commandline/trust_signer_remove.md b/docs/reference/commandline/trust_signer_remove.md index 5e425d5e6f79..2b8d52772934 100644 --- a/docs/reference/commandline/trust_signer_remove.md +++ b/docs/reference/commandline/trust_signer_remove.md @@ -5,9 +5,9 @@ Remove a signer ### Options -| Name | Type | Default | Description | -|:----------------|:-----|:--------|:----------------------------------------------------------------------| -| `-f`, `--force` | | | Do not prompt for confirmation before removing the most recent signer | +| Name | Type | Default | Description | +|:----------------|:-------|:--------|:----------------------------------------------------------------------| +| `-f`, `--force` | `bool` | | Do not prompt for confirmation before removing the most recent signer | diff --git a/docs/reference/commandline/version.md b/docs/reference/commandline/version.md index 0eec3ad17b4b..040ba1b5a436 100644 --- a/docs/reference/commandline/version.md +++ b/docs/reference/commandline/version.md @@ -118,7 +118,7 @@ and Docker Engine perform API version negotiation, and select the highest API version that is supported by both the Docker CLI and the Docker Engine. For example, if the CLI is connecting with Docker Engine version 19.03, it downgrades -to API version 1.40 (refer to the [API version matrix](https://docs.docker.com/engine/api/#api-version-matrix) +to API version 1.40 (refer to the [API version matrix](https://docs.docker.com/reference/api/engine/#api-version-matrix) to learn about the supported API versions for Docker Engine): ```console diff --git a/docs/reference/commandline/volume_ls.md b/docs/reference/commandline/volume_ls.md index e5da1c8bf083..8420080a7347 100644 --- a/docs/reference/commandline/volume_ls.md +++ b/docs/reference/commandline/volume_ls.md @@ -11,10 +11,10 @@ List volumes | Name | Type | Default | Description | |:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `--cluster` | | | Display only cluster volumes, and use cluster volume list formatting | +| `--cluster` | `bool` | | Display only cluster volumes, and use cluster volume list formatting | | [`-f`](#filter), [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `dangling=true`) | | [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | -| `-q`, `--quiet` | | | Only display volume names | +| `-q`, `--quiet` | `bool` | | Only display volume names | diff --git a/docs/reference/commandline/volume_prune.md b/docs/reference/commandline/volume_prune.md index d2671fa7a0ed..b0aa7061c524 100644 --- a/docs/reference/commandline/volume_prune.md +++ b/docs/reference/commandline/volume_prune.md @@ -7,9 +7,9 @@ Remove unused local volumes | Name | Type | Default | Description | |:------------------------------|:---------|:--------|:---------------------------------------------------| -| [`-a`](#all), [`--all`](#all) | | | Remove all unused volumes, not just anonymous ones | +| [`-a`](#all), [`--all`](#all) | `bool` | | Remove all unused volumes, not just anonymous ones | | [`--filter`](#filter) | `filter` | | Provide filter values (e.g. `label=