Compare commits

...

207 Commits

Author SHA1 Message Date
Sebastiaan van Stijn 48a2cdff97
Merge pull request #5432 from thaJeztah/27.x_backport_wsl-socket-path
[27.x backport] command: check for wsl mount path on windows
2024-09-13 10:54:31 +02:00
Jonathan A. Sternberg b48720e65e
command: check for wsl mount path on windows
This checks for the equivalent WSL mount path on windows. WSL will mount
the windows drives at `/mnt/c` (or whichever drive is being used).

This is done by parsing a UNC path with forward slashes from the unix
socket URL.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
(cherry picked from commit 38c3fef1a8)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-13 00:35:37 +02:00
Sebastiaan van Stijn c85c3df6b5
Merge pull request #5431 from thaJeztah/27.x_bump_engine2
[27.x] vendor: github.com/docker/docker bf60e5cced83 (v27.3.0-dev)
2024-09-13 00:34:34 +02:00
Sebastiaan van Stijn 74bebe2719
vendor: github.com/docker/docker bf60e5cced83 (v27.3.0-dev)
full diff: https://github.com/docker/docker/compare/v27.2.1...bf60e5cced830ee3034275c1792095fbfa40caf4

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-13 00:21:52 +02:00
Sebastiaan van Stijn 9748bef1c3
Merge pull request #5430 from thaJeztah/27.x_bump_engine_deps
[27.x backport] update dependencies for engine update
2024-09-13 00:21:32 +02:00
Sebastiaan van Stijn 2fc18b9874
vendor: google.golang.org/grpc v1.62.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit dccb8bfa5d)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-12 23:59:44 +02:00
Sebastiaan van Stijn 1a199b63da
vendor: tags.cncf.io/container-device-interface v0.8.0
Breaking change: The .ToOCI() functions in the specs-go package have been
removed. This removes the dependency on the OCI runtime specification from
the CDI specification definition itself.

What's Changed

- Add workflow to mark prs and issues as stale
- Remove the ToOCI functions from the specs-go package
- docs: add a pointer to community meetings in our docs.
- Bump spec version to v0.8.0
- Update spec version in README

Full diff: https://github.com/cncf-tags/container-device-interface/compare/v0.7.2...v0.8.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 8cdf90cd93)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-12 23:56:14 +02:00
Sebastiaan van Stijn 968341cc7d
vendor: golang.org/x/net v0.28.0
full diff: https://github.com/golang/net/compare/v0.25.0...v0.28.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit a5f15bee7a)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-12 23:56:14 +02:00
Sebastiaan van Stijn fbb0cfd86a
vendor: golang.org/x/crypto v0.26.0
full diff: https://github.com/golang/crypto/compare/v0.23.0...v0.26.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit b93fc39639)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-12 23:56:13 +02:00
Sebastiaan van Stijn 49e33a03df
vendor: golang.org/x/text v0.17.0
full diff: https://github.com/golang/text/compare/v0.15.0...v0.17.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 3a63df265f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-12 23:56:13 +02:00
Sebastiaan van Stijn 295b75e5ff
vendor: golang.org/x/term v0.23.0
full diff: https://github.com/golang/term/compare/v0.20.0...v0.23.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit c6e5341934)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-12 23:56:13 +02:00
Sebastiaan van Stijn 090d1ff555
vendor: golang.org/x/time v0.6.0
full diff: https://github.com/golang/time/compare/v0.3.0...v0.6.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 5f9fe33b6b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-12 23:56:13 +02:00
Sebastiaan van Stijn 5dde3d8570
vendor: golang.org/x/sync v0.8.0
full diff: https://github.com/golang/sync/compare/v0.7.0...v0.8.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 7074e5011f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-12 23:56:13 +02:00
Sebastiaan van Stijn d64740d347
vendor: golang.org/x/sys v0.24.0
full diff: https://github.com/golang/sys/compare/v0.22.0...v0.24.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 958fff82f1)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-12 23:56:12 +02:00
Sebastiaan van Stijn f68936ef2e
vendor: dario.cat/mergo v1.0.1
- fix: overwriteWithEmptyValue is forced to true when merging an object
  involving maps
- fix: WithoutDereference should respect non-nil struct pointers

full diff: https://github.com/darccio/mergo/compare/v1.0.0...v1.0.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit fb264ffc08)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-12 23:56:12 +02:00
Sebastiaan van Stijn 4607c883c5
vendor: github.com/moby/sys/sequential v0.6.0
full diff: https://github.com/moby/sys/compare/sequential/v0.5.0...sequential/v0.6.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit b34e8e4dff)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-12 23:53:02 +02:00
Sebastiaan van Stijn 7ff3daa446
vendor: github.com/moby/sys/symlink v0.3.0
full diff: https://github.com/moby/sys/compare/symlink/v0.2.0...symlink/v0.3.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit ea37ac9bac)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-12 23:52:59 +02:00
Sebastiaan van Stijn 1fe06dd0e7
vendor: github.com/moby/sys/signal v0.7.1
full diff: https://github.com/moby/sys/compare/signal/v0.7.0...signal/v0.7.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 435c658333)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-12 23:47:30 +02:00
Sebastiaan van Stijn b381ab1d8d
Merge pull request #5429 from thaJeztah/27.x_bump_docker
[27.x] vendor: github.com/docker/docker v27.2.1
2024-09-12 22:42:13 +02:00
Sebastiaan van Stijn 05fbfc6995
vendor: github.com/docker/docker v27.2.1
no diff: same commit, but tagged

full diff: https://github.com/docker/docker/compare/8b539b8df240...v27.2.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-12 22:16:13 +02:00
Laura Brehm fba240c5b4
Merge pull request #5426 from thaJeztah/27.x_backport_fix-panic-volume-update
[27.x backport] volume/update: require 1 argument/fix panic
2024-09-11 15:24:41 +01:00
Sebastiaan van Stijn 965699ba0f
cli/command/volume TestUpdateCmd: adjust for older error messages
The error-message changed in newer versions, and no longer includes
"exactly".

This patch adjusts the test in the meantime.

    59.13 === FAIL: cli/command/volume TestUpdateCmd (0.00s)
    59.13     update_test.go:21: assertion failed: expected error to contain "requires 1 argument", got "\"update\" requires exactly 1 argument.\nSee 'update --help'.\n\nUsage:  update [OPTIONS] [VOLUME] [flags]\n\nUpdate a volume (cluster volumes only)"

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-11 13:26:06 +02:00
Laura Brehm c65ac2d90d
volume/update: require 1 argument/fix panic
This command was declaring that it requires at least 1 argument, when it
needs exactly 1 argument. This was causing the CLI to panic when the
command was invoked with no argument:

`docker volume update`

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit daea277ee8)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-11 13:12:54 +02:00
Sebastiaan van Stijn 05fb576772
Merge pull request #5423 from thaJeztah/27.x_backport_info_no_expected_version
[27.x backport] info: stop printing "Expected" commits
2024-09-10 19:51:58 +02:00
Sebastiaan van Stijn 4be6b1f3d7
info: stop printing "Expected" commits
The `Commit` type was introduced in 2790ac68b3,
to assist triaging issues that were reported with an incorrect version of
runc or containerd. At the time, both `runc` and `containerd` were not yet
stable, and had to be built from a specific commit to guarantee compatibility.

We encountered various situations where unexpected (and incompatible) versions
of those binaries were packaged, resulting in hard to trace bug-reports.
For those situations, a "expected" version was set at compile time, to
indicate if the version installed was different from the expected version;

    docker info
    ...
    runc version: a592beb5bc4c4092b1b1bac971afed27687340c5 (expected: 69663f0bd4b60df09991c08812a60108003fa340)

Both `runc` and `containerd` are stable now, and docker 19.03 and up set the
expected version to the actual version since c65f0bd13c
and 23.0 did the same for the `init` binary b585c64e2b,
to prevent the CLI from reporting "unexpected version".

In short; the `Expected` fields no longer serves a real purpose, so we should
no longer print it.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 88ca4e958f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-10 16:41:43 +02:00
Sebastiaan van Stijn 65decb5731
Merge pull request #5419 from dvdksn/bp_5403
[27.x backport] rename plugins index file and add linkTitle
2024-09-09 11:05:09 +02:00
David Karlsson 90559a6143 chore: fix style/lint issues in deprecated.md
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit 0fcaffb7e4)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-09-09 10:54:29 +02:00
David Karlsson dbde5b3681 docs: add front matter title to deprecated.md
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit 5ca40e0a35)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-09-09 10:33:23 +02:00
David Karlsson b1e50eea92 docs: rename plugins index file and add linkTitle
We publish this page on docs.docker.com, and hugo expects index pages
for sections to be named _index.md. We currently rename the page when we
mount it to the docs repo but might as well change the filename in the
source.

Also adds a linkTitle to the page, which is a shorter title that will be
used in the sidebar navigation and breadcrumbs.

Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit 071f6f9391)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-09-09 10:33:23 +02:00
Paweł Gronowski 9e34c9bb39
Merge pull request #5414 from vvoland/vendor-docker
[27.x] vendor: github.com/docker/docker v27.2.1-dev (8b539b8df240)
2024-09-06 12:01:30 +00:00
Paweł Gronowski 324cdbca40
vendor: github.com/docker/docker v27.2.1-dev (8b539b8df240)
full diff: https://github.com/docker/docker/compare/v27.2.0...8b539b8df240

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-09-06 13:48:24 +02:00
Sebastiaan van Stijn b5290d4e0b
Merge pull request #5411 from vvoland/5410-27.x
[27.x backport] update to go1.22.7
2024-09-06 10:28:04 +02:00
Paweł Gronowski 3db9538748
update to go1.22.7
- https://github.com/golang/go/issues?q=milestone%3AGo1.22.7+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.22.6...go1.22.7

These minor releases include 3 security fixes following the security policy:

- go/parser: stack exhaustion in all Parse* functions

    Calling any of the Parse functions on Go source code which contains deeply nested literals can cause a panic due to stack exhaustion.

    This is CVE-2024-34155 and Go issue https://go.dev/issue/69138.

- encoding/gob: stack exhaustion in Decoder.Decode

    Calling Decoder.Decode on a message which contains deeply nested structures can cause a panic due to stack exhaustion.

    This is a follow-up to CVE-2022-30635.

    Thanks to Md Sakib Anwar of The Ohio State University (anwar.40@osu.edu) for reporting this issue.

    This is CVE-2024-34156 and Go issue https://go.dev/issue/69139.

- go/build/constraint: stack exhaustion in Parse

    Calling Parse on a "// +build" build tag line with deeply nested expressions can cause a panic due to stack exhaustion.

    This is CVE-2024-34158 and Go issue https://go.dev/issue/69141.

View the release notes for more information:
https://go.dev/doc/devel/release#go1.23.1

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 3bf39d25a0)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-09-05 17:53:21 +02:00
Sebastiaan van Stijn 1ab89e71fa
Merge pull request #5409 from thaJeztah/27.x_update_docker
[27.x] vendor: github.com/docker/docker v27.2.0
2024-09-05 15:02:51 +02:00
Sebastiaan van Stijn 667d9fd4df
Merge pull request #5408 from thaJeztah/27.x_backport_mod_tidy
[27.x backport] vendor.mod: put github.com/pkg/browser in the right group
2024-09-05 14:53:18 +02:00
Sebastiaan van Stijn 41e61c45d9
[27.x] vendor: github.com/docker/docker v27.2.0
Use a tagged version instead of the commit. No diff as they are the same;
https://github.com/docker/docker/compare/3ab5c7d0036c...v27.2.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-05 14:45:28 +02:00
Sebastiaan van Stijn 869df10064
vendor.mod: put github.com/pkg/browser in the right group
commit fcfdd7b91f introduced github.com/pkg/browser
as a direct dependency, but it ended up in the group for indirect dependencies.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 1b8180a405)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-05 14:40:13 +02:00
Paweł Gronowski 6feee4ab35
Merge pull request #5402 from laurazard/backport-27.x-login-not-interactive
[27.x backport] login: handle non-tty scenario consistently
2024-09-03 14:45:27 +00:00
Laura Brehm d0c1a80617
login: handle non-tty scenario consistently
Running `docker login` in a non-interactive environment sometimes errors
out if no username/pwd is provided. This handling is somewhat
inconsistent – this commit addresses that.

Before:
| `--username` | `--password` | Result                                                             |
|:------------:|:------------:| ------------------------------------------------------------------ |
|            |            |                                                                  |
|            |            | `Error: Cannot perform an interactive login from a non TTY device` |
|            |            | `Error: Cannot perform an interactive login from a non TTY device` |
|            |            | hangs                                                              |

After:
| `--username` | `--password` | Result                                                             |
|:------------:|:------------:| ------------------------------------------------------------------ |
|            |            |                                                                  |
|            |            | `Error: Cannot perform an interactive login from a non TTY device` |
|            |            | `Error: Cannot perform an interactive login from a non TTY device` |
|            |            | `Error: Cannot perform an interactive login from a non TTY device` |

It's worth calling out a separate scenario – if there are previous,
valid credentials, then running `docker login` with no username or
password provided will use the previously stored credentials, and not
error out.

```console
cat ~/.docker/config.json
{
        "auths": {
                "https://index.docker.io/v1/": {
                        "auth": "xxxxxxxxxxx"
                }
        }
}
⭑ docker login 0>/dev/null
Authenticating with existing credentials...

Login Succeeded
```

This commit also applies the same non-interactive handling logic to the
new web-based login flow, which means that now, if there are no prior
credentials stored and a user runs `docker login`, instead of initiating
the new web-based login flow, an error is returned.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit bbb6e7643d)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-09-03 15:39:17 +01:00
Sebastiaan van Stijn 383c428451
Merge pull request #5400 from vvoland/5387-27.x
[27.x backport] update to go1.22.6
2024-09-03 14:04:04 +02:00
Sebastiaan van Stijn 5f8416e541
Merge pull request #5399 from vvoland/5376-27.x
[27.x backport] oauth/api: drain timer channel on each iteration
2024-09-03 14:02:04 +02:00
Sebastiaan van Stijn 5bf5cb9ff6
update to go1.22.6
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit d7d56599ca)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-09-03 13:52:28 +02:00
Sebastiaan van Stijn 3a15d5a640
Merge pull request #5395 from thaJeztah/27.x_backport_fix_plugins_CGO_ENABLED
[27.x backport] scripts/build/plugins: don't override CGO_ENABLED set by .variables
2024-09-03 13:25:44 +02:00
Laura Brehm 1dfd11acc0
oauth/api: drain timer channel on each iteration
Previously, if while polling for oauth device-code login results a user
suspended the process (such as with CTRL-Z) and then restored it with
`fg`, an error might occur in the form of:

```
failed waiting for authentication: You are polling faster than the specified interval of 5 seconds.
```

This is due to our use of a `time.Ticker` here - if no receiver drains
the ticker channel (and timers/tickers use a buffered channel behind the
scenes), more than one tick will pile up, causing the program to "tick"
twice, in fast succession, after it is resumed.

The new implementation replaces the `time.Ticker` with a `time.Timer`
(`time.Ticker` is just a nice wrapper) and introduces a helper function
`resetTimer` to ensure that before every `select`, the timer is stopped
and it's channel is drained.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit 60d0450287)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-09-03 13:16:03 +02:00
Sebastiaan van Stijn de2b49b074
scripts/build/plugins: don't override CGO_ENABLED set by .variables
The `.variables` sets `CGO_ENABLED=1` on arm; b0c41b78d8/scripts/build/.variables (L57-L68)
And if enabled, it sets `-buildmode=pie`; b0c41b78d8/scripts/build/.variables (L79-L88)

But that looks to be conflicting with the hardcoded `CGO_ENABLED=0` in
this script, which causes the build to fail on go1.22;

    > [build-plugins 1/1] RUN --mount=ro --mount=type=cache,target=/root/.cache     xx-go --wrap &&     TARGET=/out ./scripts/build/plugins e2e/cli-plugins/plugins/*:
    0.127 Building static docker-helloworld
    0.127 + CGO_ENABLED=0
    0.127 + GO111MODULE=auto
    0.127 + go build -o /out/plugins-linux-arm/docker-helloworld -tags ' osusergo' -ldflags ' -X "github.com/docker/cli/cli/version.GitCommit=5c123b1" -X "github.com/docker/cli/cli/version.BuildTime=2024-09-02T13:52:17Z" -X "github.com/docker/cli/cli/version.Version=pr-5387" -extldflags -static' -buildmode=pie github.com/docker/cli/cli-plugins/examples/helloworld
    0.135 -buildmode=pie requires external (cgo) linking, but cgo is not enabled

This patch sets the CGO_ENABLED variable before sourcing `.variables`,
so that other variables which are conditionally set are handled correctly.

Before this PR:

    #18 [build-plugins 1/1] RUN --mount=ro --mount=type=cache,target=/root/.cache     xx-go --wrap &&     TARGET=/out ./scripts/build/plugins e2e/cli-plugins/plugins/*
    #18 0.123 Building static docker-helloworld
    #18 0.124 + CGO_ENABLED=0
    #18 0.124 + GO111MODULE=auto
    #18 0.124 + go build -o /out/plugins-linux-arm/docker-helloworld -tags ' osusergo' -ldflags ' -X "github.com/docker/cli/cli/version.GitCommit=c8c402e" -X "github.com/docker/cli/cli/version.BuildTime=2024-09-03T08:28:25Z" -X "github.com/docker/cli/cli/version.Version=pr-5381" -extldflags -static' -buildmode=pie github.com/docker/cli/cli-plugins/examples/helloworld
    ....

With this PR:

    #18 [build-plugins 1/1] RUN --mount=ro --mount=type=cache,target=/root/.cache     xx-go --wrap &&     TARGET=/out ./scripts/build/plugins e2e/cli-plugins/plugins/*
    #18 0.110 Building static docker-helloworld
    #18 0.110 + GO111MODULE=auto
    #18 0.110 + go build -o /out/plugins-linux-arm/docker-helloworld -tags '' -ldflags ' -X "github.com/docker/cli/cli/version.GitCommit=050d9d6" -X "github.com/docker/cli/cli/version.BuildTime=2024-09-03T09:19:05Z" -X "github.com/docker/cli/cli/version.Version=pr-5387"' github.com/docker/cli/cli-plugins/examples/helloworld
    ....

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9e29967960)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-03 12:56:19 +02:00
Sebastiaan van Stijn c5d846735c
Merge pull request #5394 from dvdksn/bp_5386
[27.x backport] docs: update docker login reference #5386
2024-09-03 12:55:19 +02:00
David Karlsson 6274754e66 copynit: s/WEB BASED/WEB-BASED/
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit 81744d7aa8)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-09-03 12:22:57 +02:00
David Karlsson 7a50cd0f01 docs: update docker login reference
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit 2f206fff3c)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-09-03 12:22:52 +02:00
Sebastiaan van Stijn 074dfc0f88
Merge pull request #5392 from vvoland/5345-27.x
[27.x backport] cli/connhelper: getConnectionHelper: move ssh-option funcs out of closure
2024-09-02 22:29:25 +02:00
Sebastiaan van Stijn 92423287cc
Merge pull request #5391 from vvoland/5389-27.x
[27.x backport] Dockerfile: update xx to v1.5.0
2024-09-02 22:27:57 +02:00
Sebastiaan van Stijn 1a0b6a7a44
cli/connhelper: getConnectionHelper: move ssh-option funcs out of closure
The addSSHTimeout and disablePseudoTerminalAllocation were added in commits
a5ebe2282a and f3c2c26b10,
and called inside the Dialer function, which means they're called every
time the Dialer is called. Given that the sshFlags slice is not mutated
by the Dialer, we can call these functions once.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 0fd3fb0840)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-09-02 20:57:42 +02:00
Sebastiaan van Stijn 8fcfc0b803
Dockerfile: update xx to v1.5.0
full diff: https://github.com/tonistiigi/xx/compare/v1.4.0...v1.5.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 1e6cbbc3f1)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-09-02 20:55:31 +02:00
Laura Brehm 28d2fed463
Merge pull request #5385 from vvoland/5383-27.x
[27.x backport] login: use normalized hostname when storing
2024-09-02 10:57:51 +01:00
Laura Brehm 83072c0232
login: use normalized hostname when storing
Normalization/converting the registry address to just a hostname happens
inside of `command.GetDefaultAuthConfig`. Use this value for the rest of
the login flow/storage.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit e532eead91)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-09-02 11:43:26 +02:00
Sebastiaan van Stijn 40109aa45f
Merge pull request #5380 from laurazard/dont-normalize-registry-1-backport
[27.x backport] Revert "login: normalize `registry-1.docker.io`"
2024-08-29 13:33:58 +02:00
Laura Brehm 32aadc9902
Revert "login: normalize `registry-1.docker.io`"
This reverts commit e6624676e0.

Since e6624676e0, during login, we started
normalizing `registry-1.docker.io` to `index.docker.io`. This means that
if a user logs in with `docker login -u [username]
registry-1.docker.io`, the user's credentials get stored in
credhelpers/config.json under `https://index.docker.io/v1/`.

However, while the registry code normalizes an image reference without
registry (`docker pull alpine:latest`) and image references explicitly for
`index.docker.io` (`docker pull index.docker.io/library/alpine:latest`)
to the official index server (`https://index.docker.io/v1/`), and
fetches credentials for that auth key, it does not normalize
`registry-1.docker.io`, which means pulling explicitly from there
(`docker pull registry-1.docker.io/alpine:latest`) will not use
credentials stored under `https://index.docker.io/v1/`.

As such, until changes are made to the registry/pull/push code to
normalize `registry-1.docker.io` to `https://index.docker.io/v1/`, we
should not normalize this during login.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit dab9674db9)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-08-29 12:23:27 +01:00
Paweł Gronowski 3ab4256958
Merge pull request #5374 from vvoland/vendor-docker
[27.x backport] vendor: github.com/docker/docker 3ab5c7d0036c (v27.2.0-dev)
2024-08-27 16:08:11 +02:00
Paweł Gronowski 88a49df297
vendor: github.com/docker/docker 3ab5c7d0036c (v27.2.0-dev)
full diff: b27de4ef16...3ab5c7d003

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-27 16:02:26 +02:00
Paweł Gronowski 5d17c29eb2
Merge pull request #5372 from thaJeztah/27.x_backport_fix_linting_issues
[27.x backport] Fix linting issues in preparation of Go and GolangCI-lint update
2024-08-26 17:06:00 +02:00
Sebastiaan van Stijn 64b9e4cd16
cli: rename args that collided with builtins (predeclard)
cli/required.go:33:22: param min has same name as predeclared identifier (predeclared)
    func RequiresMinArgs(min int) cobra.PositionalArgs {
                         ^
    cli/required.go:50:22: param max has same name as predeclared identifier (predeclared)
    func RequiresMaxArgs(max int) cobra.PositionalArgs {
                         ^
    cli/required.go:67:24: param min has same name as predeclared identifier (predeclared)
    func RequiresRangeArgs(min int, max int) cobra.PositionalArgs {
                           ^

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit c4a55df7c0)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-08-26 14:49:10 +02:00
Sebastiaan van Stijn 4b71d0d1af
e2e/global: fix n-constant format string in call (govet)
e2e/global/cli_test.go:217:28: printf: non-constant format string in call to gotest.tools/v3/poll.Continue (govet)
                            return poll.Continue(err.Error())
                                                 ^

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9c87891278)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-08-26 14:38:40 +02:00
Sebastiaan van Stijn 002cfcde85
cli/command: fix n-constant format string in call (govet)
cli/command/utils.go:225:29: printf: non-constant format string in call to github.com/pkg/errors.Wrapf (govet)
                return errors.Wrapf(err, fmt.Sprintf("invalid output path: %q must be a directory or a regular file", path))
                                         ^
    cli/command/manifest/cmd.go:21:33: printf: non-constant format string in call to fmt.Fprintf (govet)
                fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
                                             ^
    cli/command/service/remove.go:45:24: printf: non-constant format string in call to github.com/pkg/errors.Errorf (govet)
            return errors.Errorf(strings.Join(errs, "\n"))
                                 ^
    cli/command/service/scale.go:93:23: printf: non-constant format string in call to github.com/pkg/errors.Errorf (govet)
        return errors.Errorf(strings.Join(errs, "\n"))
                             ^
    cli/command/stack/swarm/remove.go:74:24: printf: non-constant format string in call to github.com/pkg/errors.Errorf (govet)
            return errors.Errorf(strings.Join(errs, "\n"))
                                 ^

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit f101f07a7b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-08-26 14:38:40 +02:00
Sebastiaan van Stijn d8af7812b5
cli/command/system: remove redundant nil-check (gosimple)
cli/command/system/info.go:375:5: S1009: should omit nil check; len() for []github.com/docker/docker/api/types/system.NetworkAddressPool is defined as zero (gosimple)
        if info.DefaultAddressPools != nil && len(info.DefaultAddressPools) > 0 {
           ^

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit cc1d7b7ac9)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-08-26 14:38:37 +02:00
Sebastiaan van Stijn f042ddb5c9
Merge pull request #5371 from vvoland/vendor-docker
vendor: github.com/docker/docker b27de4ef1634 (v27.2.0-dev)
2024-08-26 14:33:00 +02:00
Paweł Gronowski 8e94ed15e6
vendor: github.com/docker/docker b27de4ef1634 (v27.2.0-dev)
full diff: 9942d656ba...b27de4ef16

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-26 14:02:54 +02:00
Sebastiaan van Stijn 7a82aeeeba
Merge pull request #5368 from dvdksn/27x_5360
[27.x backport] update link to engine api reference
2024-08-22 18:01:29 +02:00
David Karlsson 24837f9260 chore: update link to docker engine api reference
Engine API reference page is moving to /reference/api/engine

Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit c974a83391)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-08-22 15:18:55 +02:00
Paweł Gronowski 5805df0205
Merge pull request #5365 from vvoland/5363-27.x
[27.x backport] cli/formatter: bracket IPv6 addrs prepended to ports
2024-08-22 14:23:10 +02:00
David Karlsson fb20f009f7
Merge pull request #5366 from dvdksn/27x_f1befabe9f1c979d94c39eeb7020e106b3c1e6a6
[27.x backport] use gh alert syntax for callouts
2024-08-21 15:22:02 +02:00
Albin Kerouanton 6ceb0aba82
cli/formatter: bracket IPv6 addrs prepended to ports
On `docker ps`, port bindings with an IPv6 HostIP should have their
addresses put into brackets when joining them to their ports.

RFC 3986 (Section 3.2.2) stipulates that IPv6 addresses should be
enclosed within square brackets. This RFC is only about URIs. However,
doing so here helps user identifier what's part of the IP address and
what's the port. It also makes it easier to copy/paste that
'[addr]:port' into other software (including browsers).

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
(cherry picked from commit 964155cd27)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-21 11:45:56 +02:00
David Karlsson 2d7b8998c4 docs: use gh alert syntax for callouts
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit f1befabe9f)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-08-21 11:41:15 +02:00
Sebastiaan van Stijn cabd410a1a
Merge pull request #5362 from laurazard/27.x-backport-oauth-escape-hatch
[27.x backport] login: add oauth escape hatch
2024-08-20 16:11:01 +02:00
Laura Brehm a58af379e1
login: add e2e tests for oauth + escape hatch
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit a327476f7f)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-08-20 12:50:57 +01:00
Laura Brehm 1b3fa65759
login: add oauth escape hatch
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit 846ecf59ff)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-08-20 12:50:49 +01:00
David Karlsson cf01923519
Merge pull request #5348 from dvdksn/backport_update_build_context_link
[27.x backport] docs: update link to moved build context doc
2024-08-19 17:48:55 +02:00
Paweł Gronowski a0d7f0dbd3
Merge pull request #5358 from vvoland/5356-27.x
[27.x backport] list/tree: No extra spacing for graphdriver
2024-08-19 13:47:05 +02:00
Paweł Gronowski 0c4e7478e2
list/tree: No extra spacing for graphdriver
Don't output the extra spacing around the images when none of the
top-level image entries has any children.

This makes the list look better when ran against the graphdrivers image
store.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 7b91647943)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-19 13:28:13 +02:00
Paweł Gronowski 60ce3fbc96
Merge pull request #5353 from vvoland/4982-27.x
[27.x backport] image/list: Add `--tree` flag
2024-08-16 19:51:04 +02:00
Paweł Gronowski 7902b52714
list/tree: Print <untagged> as dangling image name
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 351249dce9)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:38 +02:00
Paweł Gronowski 7196200fc2
list/tree: Fix some escape codes included in nonTTY
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 6979ab073c)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:36 +02:00
Paweł Gronowski f42fa0b8e1
list/tree: Add spacing before the content and first image
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit a9b78da546)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:34 +02:00
Paweł Gronowski b719b10257
list/tree: Capitalize column headers
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 0242a1e3c6)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:33 +02:00
Paweł Gronowski ab55d75cf5
list/tree: Add an experimental warning
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit d417d06682)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:32 +02:00
Paweł Gronowski 324cc5d30f
list/tree: Sort by created date
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit b1a08f7841)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:30 +02:00
Paweł Gronowski 44a9ffa0ad
list/tree: Align number right, text left
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 18ab78882c)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:29 +02:00
Paweł Gronowski ba43ae0bd2
cli/tree: Add `Content size` column
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit ea8aafcd9e)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:28 +02:00
Paweł Gronowski 99b647cfca
image/list: Add `--tree` flag
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit be11b74ee9)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 19:40:26 +02:00
Paweł Gronowski f90dc28f1e
Merge pull request #5354 from vvoland/vendor-docker
[27.x] vendor: github.com/docker/docker v27.2.0-dev (9942d656bade)
2024-08-16 19:39:57 +02:00
Paweł Gronowski 26536d1145
vendor: github.com/docker/docker v27.2.0-dev (9942d656bade)
full diff: f9522e5e96...9942d656ba

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-16 18:59:59 +02:00
Paweł Gronowski c5e733becc
Merge pull request #5349 from laurazard/27.x-backport-oauth-login
[27.x backport] auth: add support for oauth device-code login
2024-08-16 18:13:24 +02:00
Paweł Gronowski 7227402d94
Merge pull request #5351 from laurazard/backport-27.x-disable-pseudoterminal-ssh
[27.x backport] disable pseudoterminal creation
2024-08-16 18:12:10 +02:00
Archimedes Trajano 83f6ca4a73
disable pseudoterminal creation
avoided the join, also did manual iteration

added test, also added reflect for the DeepEqual comparison

Signed-off-by: Archimedes Trajano <developer@trajano.net>
(cherry picked from commit f3c2c26b10)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-08-16 14:11:10 +01:00
Laura Brehm ad7912a846
fallback to regular login if oauth login fails to start
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit c3fe7bc336)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-08-16 10:09:41 +01:00
Laura Brehm afb5e143b1
login: normalize `registry-1.docker.io`
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit e6624676e0)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-08-16 10:09:40 +01:00
Laura Brehm b8a38fd22d
Refactor `cli/command/registry`
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit 6e4818e7d6)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-08-16 10:09:39 +01:00
Laura Brehm 0c29d6bac1
auth: add support for oauth device-code login
This commit adds support for the oauth [device-code](https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow)
login flow when authenticating against the official registry.

This is achieved by adding `cli/internal/oauth`, which contains code to manage
interacting with the Docker OAuth tenant (`login.docker.com`), including launching
the device-code flow, refreshing access using the refresh-token, and logging out.

The `OAuthManager` introduced here is also made available through the `command.Cli`
interface method `OAuthManager()`.

In order to maintain compatibility with any clients manually accessing
the credentials through `~/.docker/config.json` or via credential
helpers, the added `OAuthManager` uses the retrieved access token to
automatically generate a PAT with Hub, and store that in the
credentials.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit fcfdd7b91f)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-08-16 10:09:38 +01:00
David Karlsson 3eaf30278f docs: update link to moved build context doc
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit 2dd4eb06ae)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-08-13 12:17:58 +02:00
Paweł Gronowski d01f264bcc
Merge pull request #5333 from thaJeztah/27.x_bump_engine
[27.x] vendor: github.com/docker/docker f9522e5e96c3 (v27.1.2-dev) (removes containerd dependency)
2024-08-12 13:34:32 +02:00
Sebastiaan van Stijn 65dec14ac0
vendor: github.com/docker/docker f9522e5e96c3 (v27.1.2-dev)
Removes dependency on containerd, as the userns package was migrated
to the github.com/moby/sys/userns module.

- full diff: https://github.com/docker/docker/compare/v27.1.1...f9522e5e96c3

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-12 13:18:02 +02:00
Paweł Gronowski 1f80c54b51
Merge pull request #5339 from thaJeztah/27.x_backport_fix_bps_limit
[27.x backport] run: fix GetList return empty issue for throttledevice
2024-08-12 11:57:35 +02:00
David Karlsson 33573e20bc
Merge pull request #5343 from dvdksn/cp-docs-manuals-refactor-linkfix
[27.x backport] cherry-pick doc linkfixes due to refactor
2024-08-12 10:11:43 +02:00
David Karlsson 73452e316f docs: update internal links after refactor
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit d4a362aa1c)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-08-11 16:58:39 +02:00
David Karlsson bcd90be73a docs: fix link to http proxy document
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit 78a8fba2cc)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-08-11 16:46:02 +02:00
Sebastiaan van Stijn f62c68eedd
Merge pull request #5337 from vvoland/5327-27.x
[27.x backport] plugins: don't panic on Close if PluginServer nil
2024-08-09 20:03:37 +02:00
Jianyong Wu 946d1097b8
run: fix GetList return empty issue for throttledevice
Test "--device-read-bps" "--device-write-bps" will fail. The root
cause is that GetList helper return empty as its local variable
initialized to zero size.

This patch fix it by setting the related slice size to non-zero.

Signed-off-by: Jianyong Wu <wujianyong@hygon.cn>
Fixes: #5321
(cherry picked from commit 73e78a5822)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-08-09 19:47:08 +02:00
Sebastiaan van Stijn 096e42b366
Merge pull request #5335 from vvoland/5310-27.x
[27.x backport] gha: set permissions to read-only by default
2024-08-09 10:54:36 +02:00
Laura Brehm 984ef9072c
plugins: don't panic on Close if PluginServer nil
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit 9c4480604e)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-09 10:48:40 +02:00
Sebastiaan van Stijn 30c7951192
Merge pull request #5334 from vvoland/5289-27.x
[27.x backport] docs: refresh image versions in examples
2024-08-09 10:48:38 +02:00
Sebastiaan van Stijn 54135b0724
gha: set permissions to read-only by default
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit e4d99b4b60)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-09 10:44:34 +02:00
Sebastiaan van Stijn 40707e17b8
docs: refresh image versions in examples
use current LTS versions of ubuntu where suitable, remove uses of
ubuntu:23.10 (which reache EOL), and and update some other examples
to use more current versions.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit b36522b473)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-09 10:42:40 +02:00
Sebastiaan van Stijn edd71d77c7
vendor: golang.org/x/sys v0.22.0
full diff: https://github.com/golang/sys/compare/v0.21.0...v0.22.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 501904d48f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-08-08 13:57:57 +02:00
Paweł Gronowski 9593373f9e
Merge pull request #5325 from vvoland/5324-27.x
[27.x backport] update to go1.21.13
2024-08-08 13:27:04 +02:00
Laura Brehm 5761e662f1
Merge pull request #5326 from vvoland/5303-27.x
[27.x backport] tests/run: fix flaky `RunAttachTermination` test
2024-08-07 12:31:40 +01:00
Laura Brehm c7f3031f74
tests/run: fix flaky `RunAttachTermination` test
This test was just incorrect (and testing incorrect
behavior): it was checking that `docker run` exited with a `context
canceled` error after signalling the CLI/cancelling the command's
context, but this was incorrect (and was fixed in
991b1303da - which was when this test
started failing).

However, since this test assertion was happening inside of a goroutine,
it would sometimes pass if this assertion didn't get to run before the
test suite terminated. It was flaky because sometimes this assertion
inside the goroutine did get to execute, but after the test finished
execution, which is a big no-no.

As an aside, assertions inside goroutines are generally bad, and `govet`
even has a linter for this (but it only catches `t.Fatal` and `t.FailNow`
calls and not `assert.Xx`.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit eac83574c1)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-07 12:05:30 +02:00
Paweł Gronowski 53cb00a818
update to go1.21.13
- https://github.com/golang/go/issues?q=milestone%3AGo1.21.13+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.21.12...go1.21.13

go1.21.13 (released 2024-08-06) includes fixes to the go command, the
covdata command, and the bytes package. See the [Go 1.21.13 milestone](https://github.com/golang/go/issues?q=milestone%3AGo1.21.13+label%3ACherryPickApproved)
on our issue tracker for details.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 434d8b75e8)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-07 11:50:29 +02:00
Paweł Gronowski a35c363ffc
Merge pull request #5302 from laurazard/27-attach-exit-code
[27.1 backport] attach: wait for exit code from `ContainerWait`
2024-07-26 17:21:35 +02:00
Laura Brehm 1cf3637198
attach: wait for exit code from `ContainerWait`
Such as with `docker run`, if a user CTRL-Cs while attached to a
container, we should forward the signal and wait for the exit from
`ContainerWait`, instead of just returning.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit 7b46bfc5ac)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-07-26 15:48:37 +01:00
Sebastiaan van Stijn fd3157bf35
Merge pull request #5296 from vvoland/5295-27.0
[27.1 backport] attach: don't return context cancelled error
2024-07-25 09:59:38 +02:00
Laura Brehm dfb8f2155a
attach: don't return context cancelled error
In 3f0d90a2a9 we introduced a global
signal handler and made sure all the contexts passed into command
execution get (appropriately) cancelled when we get a SIGINT.

Due to that change, and how we use this context during `docker attach`,
we started to return the context cancelation error when a user signals
the running `docker attach`.

Since this is the intended behavior, we shouldn't return an error, so
this commit adds checks to ignore this specific error in this case.

Also adds a regression test.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit 66aa0f672c)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-07-25 09:51:11 +02:00
Sebastiaan van Stijn 2e506cbb10
Merge pull request #5293 from laurazard/27-backport-flaky-tests
[27.0 backport] tests: fix flaxy TestCloseRunningCommand test
2024-07-24 13:38:49 +02:00
Laura Brehm 7f02bc9704
tests: fix other flaky `connhelper` tests
Follow up to cc68c66c95 (there were more
tests with incorrect syntax).

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit 4a7388f0dd)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-07-24 12:19:22 +01:00
Laura Brehm 8a6f7d849d
tests: fix flaxy `TestCloseRunningCommand` test
Looks like this test was failing due to bad syntax on the `while` loop,
which caused it to die after 1 second. If the test took a bit longer,
the process would be dead before the following assertions run, causing
the test to fail/be flaky.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit cc68c66c95)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-07-24 12:19:04 +01:00
Paweł Gronowski 1b2782ef64
Merge pull request #5267 from thaJeztah/27.1_bump_engine
[27.1] vendor: github.com/docker/docker v27.1.1
2024-07-24 10:50:40 +02:00
Sebastiaan van Stijn a74040315e
vendor: github.com/docker/docker v27.1.1
no changes in vendored files

full diff: https://github.com/docker/docker/compare/v27.1.0...v27.1.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-24 00:23:56 +02:00
Sebastiaan van Stijn b889b2562c
vendor: github.com/docker/docker v27.1.0
full diff:

- https://github.com/docker/docker/compare/v27.0.3....v27.1.0
- google.golang.org/genproto/googleapis/rpc 49dd2c1f3d...995d672761
- google.golang.org/genproto/googleapis/api: 49dd2c1f3d...83a465c022

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-23 13:37:57 +02:00
Sebastiaan van Stijn b24c7417e4
vendor: github.com/containerd/containerd v1.7.20
no changes in vendored code

full diff: https://github.com/containerd/containerd/compare/v1.7.19...v1.7.20

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 401048b9cb)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-23 13:37:29 +02:00
Sebastiaan van Stijn 63125853e3
Merge pull request #5274 from thaJeztah/27.1_backport_compose_oom
[27.1 backport] Add OomScoreAdj to "docker service create" and "docker stack"
2024-07-19 19:35:01 +02:00
plaurent c599566439
Allow for OomScoreAdj
Signed-off-by: plaurent <patrick@saint-laurent.us>
(cherry picked from commit aa2c2cd906)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 19:06:39 +02:00
Sebastiaan van Stijn fb19def3ce
Merge pull request #5271 from thaJeztah/27.1_backport_custom_headers_env_var
[27.1 backport] add support for DOCKER_CUSTOM_HEADERS env-var (experimental)
2024-07-19 16:44:14 +02:00
Sebastiaan van Stijn bccd4786f7
Merge pull request #5270 from thaJeztah/27.1_backport_test_spring_cleaning
[27.1] test spring-cleaning
2024-07-19 15:09:56 +02:00
Sebastiaan van Stijn 8992378c87
add support for DOCKER_CUSTOM_HEADERS env-var (experimental)
This environment variable allows for setting additional headers
to be sent by the client. Headers set through this environment
variable are added to headers set through the config-file (through
the HttpHeaders field).

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.

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).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 6638deb9d6)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 15:07:06 +02:00
Sebastiaan van Stijn f90273c340
Merge pull request #5269 from thaJeztah/27.1_backport_add_macos_apple_silicon
[27.1 backport] gha: update to macOS 13, add macOS 14 arm64 (Apple Silicon M1)
2024-07-19 13:43:25 +02:00
Sebastiaan van Stijn ca9636a1c3
test spring-cleaning
This makes a quick pass through our tests;

Discard output/err
----------------------------------------------

Many tests were testing for error-conditions, but didn't discard output.
This produced a lot of noise when running the tests, and made it hard
to discover if there were actual failures, or if the output was expected.
For example:

    === RUN   TestConfigCreateErrors
    Error: "create" requires exactly 2 arguments.
    See 'create --help'.

    Usage:  create [OPTIONS] CONFIG file|- [flags]

    Create a config from a file or STDIN
    Error: "create" requires exactly 2 arguments.
    See 'create --help'.

    Usage:  create [OPTIONS] CONFIG file|- [flags]

    Create a config from a file or STDIN
    Error: error creating config
    --- PASS: TestConfigCreateErrors (0.00s)

And after discarding output:

    === RUN   TestConfigCreateErrors
    --- PASS: TestConfigCreateErrors (0.00s)

Use sub-tests where possible
----------------------------------------------

Some tests were already set-up to use test-tables, and even had a usable
name (or in some cases "error" to check for). Change them to actual sub-
tests. Same test as above, but now with sub-tests and output discarded:

    === RUN   TestConfigCreateErrors
    === RUN   TestConfigCreateErrors/requires_exactly_2_arguments
    === RUN   TestConfigCreateErrors/requires_exactly_2_arguments#01
    === RUN   TestConfigCreateErrors/error_creating_config
    --- PASS: TestConfigCreateErrors (0.00s)
        --- PASS: TestConfigCreateErrors/requires_exactly_2_arguments (0.00s)
        --- PASS: TestConfigCreateErrors/requires_exactly_2_arguments#01 (0.00s)
        --- PASS: TestConfigCreateErrors/error_creating_config (0.00s)
    PASS

It's not perfect in all cases (in the above, there's duplicate "expected"
errors, but Go conveniently adds "#01" for the duplicate). There's probably
also various tests I missed that could still use the same changes applied;
we can improve these in follow-ups.

Set cmd.Args to prevent test-failures
----------------------------------------------

When running tests from my IDE, it compiles the tests before running,
then executes the compiled binary to run the tests. Cobra doesn't like
that, because in that situation `os.Args` is taken as argument for the
command that's executed. The command that's tested now sees the test-
flags as arguments (`-test.v -test.run ..`), which causes various tests
to fail ("Command XYZ does not accept arguments").

    # compile the tests:
    go test -c -o foo.test

    # execute the test:
    ./foo.test -test.v -test.run TestFoo
    === RUN   TestFoo
    Error: "foo" accepts no arguments.

The Cobra maintainers ran into the same situation, and for their own
use have added a special case to ignore `os.Args` in these cases;
https://github.com/spf13/cobra/blob/v1.8.1/command.go#L1078-L1083

    args := c.args

    // Workaround FAIL with "go test -v" or "cobra.test -test.v", see #155
    if c.args == nil && filepath.Base(os.Args[0]) != "cobra.test" {
        args = os.Args[1:]
    }

Unfortunately, that exception is too specific (only checks for `cobra.test`),
so doesn't automatically fix the issue for other test-binaries. They did
provide a `cmd.SetArgs()` utility for this purpose
https://github.com/spf13/cobra/blob/v1.8.1/command.go#L276-L280

    // SetArgs sets arguments for the command. It is set to os.Args[1:] by default, if desired, can be overridden
    // particularly useful when testing.
    func (c *Command) SetArgs(a []string) {
        c.args = a
    }

And the fix is to explicitly set the command's args to an empty slice to
prevent Cobra from falling back to using `os.Args[1:]` as arguments.

    cmd := newSomeThingCommand()
    cmd.SetArgs([]string{})

Some tests already take this issue into account, and I updated some tests
for this, but there's likely many other ones that can use the same treatment.

Perhaps the Cobra maintainers would accept a contribution to make their
condition less specific and to look for binaries ending with a `.test`
suffix (which is what compiled binaries usually are named as).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit ab230240ad)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 13:37:27 +02:00
Sebastiaan van Stijn ad47d2a2c1
gha: update to macOS 13, add macOS 14 arm64 (Apple Silicon M1)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9617e8d0ce)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 13:28:07 +02:00
Paweł Gronowski a2a0fb73ea
Merge pull request #5263 from thaJeztah/27.1_backport_relax_pr_check
[27.1 backport] gha: check-pr-branch: verify major version only
2024-07-19 13:25:57 +02:00
Sebastiaan van Stijn 16d6c90a94
Merge pull request #5265 from thaJeztah/27.1_backport_bump_buildx_compose
[27.0 backport] Dockerfile: update buildx to v0.16.1, compose to v2.29.0
2024-07-19 12:55:55 +02:00
Sebastiaan van Stijn f7be714467
gha: check-pr-branch: verify major version only
We'll be using release branches for minor version updates, so instead
of (e.g.) a 27.0 branch, we'll be using 27.x and continue using the
branch for minor version updates.

This patch changes the validation step to only compare against the
major version.

Co-authored-by: Cory Snider <corhere@gmail.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 6d8fcbb233)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 12:48:22 +02:00
Sebastiaan van Stijn 33bf62c4cb
Merge pull request #5261 from thaJeztah/27.1_backport_completion_enhancements
[27.0 backport] assorted fixes and enhancements for shell-completion
2024-07-19 12:05:33 +02:00
Sebastiaan van Stijn 7e972d2f5c
Merge pull request #5260 from thaJeztah/27.1_backport_fix-cli-login
[27.0 backport] fix: ctx cancellation on login prompt
2024-07-19 12:05:16 +02:00
Sebastiaan van Stijn a0791d0e8a
Merge pull request #5266 from thaJeztah/27.1_backport_update_dependencies
[27.0 backport] update dependencies for v27.1
2024-07-19 10:46:06 +02:00
Sebastiaan van Stijn 259d5e0f57
Dockerfile: update compose to v2.29.0
This is the version used in the dev-container, and for testing.

release notes: https://github.com/docker/compose/releases/tag/v2.29.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 77c0d83602)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 02:46:00 +02:00
Sebastiaan van Stijn c365c3cc8a
Dockerfile: update buildx to v0.16.1
This is the version used in the dev-container, and for testing.

release notes:
https://github.com/docker/buildx/releases/tag/v0.16.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit d00e1abf55)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 02:46:00 +02:00
Sebastiaan van Stijn 54c8eb7941
docs: fix typos and version for cli-docs-tool scripts
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 64a3fb82dc)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 02:04:44 +02:00
Sebastiaan van Stijn 5ab44a4690
vendor: github.com/docker/cli-docs-tool v0.8.0
no changes in vendored code

full diff: https://github.com/docker/cli-docs-tool/compare/v0.7.0...v0.8.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit e3e9b99015)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 02:04:44 +02:00
Sebastiaan van Stijn 33e5c87957
vendor: google.golang.org/genproto/googleapis/api 49dd2c1f3d0b
No changes in vendored files. This one got out of sync with the other modules
from the same repository.

full diff: d307bd883b...49dd2c1f3d

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit a77ba7eda8)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 02:04:24 +02:00
Sebastiaan van Stijn 674f8d2979
vendor: github.com/prometheus/procfs v0.15.1
full diff: https://github.com/prometheus/procfs/compare/v0.12.0...v0.15.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit caa5d15e98)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 02:04:24 +02:00
Sebastiaan van Stijn 2794ebd599
vendor: github.com/containerd/containerd v1.7.19
no changes in vendored code

full diff: https://github.com/containerd/containerd/compare/v1.7.18...v1.7.19

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 0f712827f1)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 02:04:24 +02:00
Sebastiaan van Stijn 76863b46b7
vendor: golang.org/x/sync v0.7.0
no changes in vendored code

full diff: https://github.com/golang/sync/compare/v0.6.0...v0.7.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit b28a1cd029)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 02:04:23 +02:00
Sebastiaan van Stijn e7bcae9053
vendor: golang.org/x/net v0.25.0
full diff: https://github.com/golang/net/compare/v0.24.0...v0.25.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit a0c4e56dea)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 02:03:51 +02:00
Sebastiaan van Stijn 3577a17ce1
vendor: golang.org/x/crypto v0.23.0
no changes in vendored code

full diff: https://github.com/golang/crypto/compare/v0.22.0...v0.23.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 723130d7fe)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 02:03:51 +02:00
Sebastiaan van Stijn a8e2130643
vendor: golang.org/x/text v0.15.0
no changes in vendored files

full diff: https://github.com/golang/text/compare/v0.14.0...v0.15.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit d33ef57dcb)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 02:03:51 +02:00
Sebastiaan van Stijn 4f1a67e06b
vendor: golang.org/x/sys v0.21.0
full diff: https://github.com/golang/sys/compare/v0.19.0...v0.21.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 21dbedd419)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 02:03:51 +02:00
Sebastiaan van Stijn dfe3c0c074
vendor: github.com/klauspost/compress v1.17.9
full diff: https://github.com/klauspost/compress/compare/v1.17.4...v1.17.9

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit f8e7c0a0d6)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 02:03:50 +02:00
Sebastiaan van Stijn 66ec7142ae
cli/command/container: add completion for --stop-signal
With this patch:

    docker run --stop-signal <TAB>
    ABRT  IOT      RTMAX-4   RTMIN     RTMIN+11  TSTP
    ALRM  KILL     RTMAX-5   RTMIN+1   RTMIN+12  TTIN
    BUS   PIPE     RTMAX-6   RTMIN+2   RTMIN+13  TTOU
    CHLD  POLL     RTMAX-7   RTMIN+3   RTMIN+14  URG
    CLD   PROF     RTMAX-8   RTMIN+4   RTMIN+15  USR1
    CONT  PWR      RTMAX-9   RTMIN+5   SEGV      USR2
    FPE   QUIT     RTMAX-10  RTMIN+6   STKFLT    VTALRM
    HUP   RTMAX    RTMAX-11  RTMIN+7   STOP      WINCH
    ILL   RTMAX-1  RTMAX-12  RTMIN+8   SYS       XCPU
    INT   RTMAX-2  RTMAX-13  RTMIN+9   TERM      XFSZ
    IO    RTMAX-3  RTMAX-14  RTMIN+10  TRAP

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit b1c0ddca02)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 01:56:26 +02:00
Sebastiaan van Stijn ab8a2c0716
cli/command/container: add completion for --volumes-from
With this patch:

    docker run --volumes-from amazing_nobel
    amazing_cannon     boring_wozniak         determined_banzai
    elegant_solomon    reverent_booth         amazing_nobel

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit d6f78cdbb1)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 01:56:25 +02:00
Sebastiaan van Stijn d11e73d6e6
cli/command/container: add completion for --restart
With this patch:

    docker run --restart <TAB>
    always  no  on-failure  unless-stopped

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 7fe7223c2c)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 01:56:25 +02:00
Sebastiaan van Stijn 36802176a7
cli/command/container: add completion for --cap-add, --cap-drop
With this patch:

    docker run --cap-add <TAB>
    ALL                     CAP_KILL                CAP_SETUID
    CAP_AUDIT_CONTROL       CAP_LEASE               CAP_SYSLOG
    CAP_AUDIT_READ          CAP_LINUX_IMMUTABLE     CAP_SYS_ADMIN
    CAP_AUDIT_WRITE         CAP_MAC_ADMIN           CAP_SYS_BOOT
    CAP_BLOCK_SUSPEND       CAP_MAC_OVERRIDE        CAP_SYS_CHROOT
    CAP_BPF                 CAP_MKNOD               CAP_SYS_MODULE
    CAP_CHECKPOINT_RESTORE  CAP_NET_ADMIN           CAP_SYS_NICE
    CAP_CHOWN               CAP_NET_BIND_SERVICE    CAP_SYS_PACCT
    CAP_DAC_OVERRIDE        CAP_NET_BROADCAST       CAP_SYS_PTRACE
    CAP_DAC_READ_SEARCH     CAP_NET_RAW             CAP_SYS_RAWIO
    CAP_FOWNER              CAP_PERFMON             CAP_SYS_RESOURCE
    CAP_FSETID              CAP_SETFCAP             CAP_SYS_TIME
    CAP_IPC_LOCK            CAP_SETGID              CAP_SYS_TTY_CONFIG
    CAP_IPC_OWNER           CAP_SETPCAP             CAP_WAKE_ALARM

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit f30158dbf8)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 01:56:25 +02:00
Sebastiaan van Stijn 3926ed6b24
cli/context/store: Names(): fix panic when called with nil-interface
Before this, it would panic when a nil-interface was passed.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit e4dd8b1898)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 01:56:25 +02:00
Sebastiaan van Stijn 787caf2fe4
cmd/docker: fix completion for --context
registerCompletionFuncForGlobalFlags was called from newDockerCommand,
at which time no context-store is initialized yet, so it would return
a nil value, probably resulting in `store.Names` to panic, but these
errors are not shown when running the completion. As a result, the flag
completion would fall back to completing from filenames.

This patch changes the function to dynamically get the context-store;
this fixes the problem mentioned above, because at the time the completion
function is _invoked_, the CLI is fully initialized, and does have a
context-store available.

A (non-exported) interface is defined to allow the function to accept
alternative implementations (not requiring a full command.DockerCLI).

Before this patch:

    docker context create one
    docker context create two

    docker --context <TAB>
    .DS_Store                   .idea/                      Makefile
    .dockerignore               .mailmap                    build/
    ...

With this patch:

    docker context create one
    docker context create two

    docker --context <TAB>
    default  one      two

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 42b68a3ed7)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 01:56:24 +02:00
Sebastiaan van Stijn 4473f48847
cli/command/container: provide flag-completion for "docker create"
"docker run" and "docker create" are mostly identical, so we can copy
the same completion functions,

We could possibly create a utility for this (similar to `addFlags()` which
configures both commands with the flags they share). I considered combining
his with `addFlags()`, but that utility is also used in various tests, in
which we don't need this feature, so keeping that for a future exercise.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 162d9748b9)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 01:56:24 +02:00
Sebastiaan van Stijn 7a4062a4a9
cli/command/completion: add FromList utility
It's an alias for cobra.FixedCompletions but takes a variadic list
of strings, so that it's not needed to construct an array for this.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 5e7bcbeac6)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 01:56:24 +02:00
Sebastiaan van Stijn d914a3f97e
cli/command/completion: add EnvVarNames utility
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

Before this patch:

    docker run --env GO
    GO111MODULE=auto        GOLANG_VERSION=1.21.12  GOPATH=/go              GOTOOLCHAIN=local

With this patch:

    docker run --env GO<tab>
    GO111MODULE     GOLANG_VERSION  GOPATH          GOTOOLCHAIN

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit e3427f341b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 01:56:24 +02:00
Sebastiaan van Stijn 68fe829c96
cli/command/completion: add FileNames utility
This is just a convenience function to allow defining completion to
use the default (complete with filenames and directories).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9207ff1046)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 01:56:24 +02:00
Sebastiaan van Stijn d60252954b
cli/command/container: NewRunCommand: slight cleanup of completion
- explicitly suppress unhandled errors
- remove names for unused arguments

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit eed0e5b02a)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 01:56:23 +02:00
Sebastiaan van Stijn 6cd1f6f26d
Makefile: add completion target
Add a "completion" target to install the generated completion
scripts inside the dev-container. As generating this script
depends on the docker binary, it calls "make binary" first.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 3f3ecb94c5)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 01:55:36 +02:00
Sebastiaan van Stijn 338e7a604c
Dockerfile.dev: install bash-completion in dev container
It's not initialized, because there's no `docker` command installed
by default, but at least this makes sure that the basics are present
for testing.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 3d80b7b0a7)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 01:55:36 +02:00
Dan Wallis 5dea9d881c
Enable completion for 'image' sub commands
Signed-off-by: Dan Wallis <dan@wallis.nz>
(cherry picked from commit c7d46aa7a1)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 01:55:35 +02:00
Alano Terblanche 4d67ef09c8
fix: ctx cancellation on login prompt
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit c15ade0c64)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-19 01:46:16 +02:00
Paweł Gronowski 69a2c9fb6d
Merge pull request #5250 from Benehiko/27.0-container-ctx
[27.0 backport] fix: container stream should not be terminated by ctx
2024-07-12 15:59:13 +02:00
Alano Terblanche 333103d93f
chore: restore ctx without cancel on container run
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
2024-07-12 15:04:38 +02:00
Alano Terblanche d36bdb0d84
test: e2e SIGTERM attached container on `docker run`
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
2024-07-12 15:04:38 +02:00
Alano Terblanche 5777558c77
fix: container stream should not be terminated by ctx
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
2024-07-12 15:04:26 +02:00
Paweł Gronowski 52848fb798
Merge pull request #5248 from vvoland/v27.0-5246
[27.0 backport] push: Don't default to `DOCKER_DEFAULT_PLATFORM`, improve message
2024-07-11 12:27:17 +02:00
Paweł Gronowski bd43ca786b
push: Improve note message and colors
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 6c04adc05e)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-07-11 12:06:02 +02:00
Paweł Gronowski 330a8e4e23
c8d: Remove `docker convert` mention
It's not merged yet.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit d40199440d)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-07-11 12:05:59 +02:00
Paweł Gronowski e0b44d6d3f
push: Don't default to DOCKER_DEFAULT_PLATFORM
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 4ce6e50e2e)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-07-11 12:05:57 +02:00
Sebastiaan van Stijn d41cb083c3
Merge pull request #5237 from dvdksn/backport_cli_reference_overview_base_cmd
[27.0 backport] cli reference overview base cmd
2024-07-05 19:55:31 +02:00
David Karlsson 0a8bb6e5b4 chore: regenerate docs
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit dc22572e3e)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-07-05 15:20:48 +02:00
David Karlsson c777dd16df docs: update cli-docs-tool (v0.8.0)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit 8549d250f6)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-07-05 15:20:45 +02:00
David Karlsson eea26c50dd docs: update links to docker cli reference
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit 3d4c12af73)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-07-05 15:20:42 +02:00
David Karlsson 1cf2c4efb3 docs: regenerate base command
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit bf33c8f10a)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-07-05 15:20:39 +02:00
David Karlsson c2b9c1474a docs: align heading structure for base command
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit b0650f281e)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-07-05 15:20:35 +02:00
David Karlsson 598442d37d docs: remove frontmatter for base command
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit cfea2353b3)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-07-05 15:20:32 +02:00
David Karlsson c964b80e53 docs: rename cli.md to docker.md (base command)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit 03961449aa)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-07-05 15:20:27 +02:00
David Karlsson 0ca7be015e docs: remove empty docker base command reference
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit a683823383)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-07-05 15:20:22 +02:00
Paweł Gronowski 59fb099da2
Merge pull request #5227 from dvdksn/backport_buildx_canonical
[27.0 backport] docs: make buildx build the canonical reference doc
2024-07-04 11:20:46 +02:00
David Karlsson 27bf78d335 docs: make buildx build the canonical reference doc
Move common flag descriptions to the buildx build reference, and make
that page the canonical page in docs. Also rewrite some content in
image_build to make clear that this page is only for the legacy builder.

Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit e91f0ded9c)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-07-04 09:19:10 +02:00
Sebastiaan van Stijn 71b756be32
Merge pull request #5219 from vvoland/v27.0-5218
[27.0 backport] update to go1.21.12
2024-07-03 12:29:38 +02:00
Sebastiaan van Stijn 63a27bc9b6
Merge pull request #5208 from thaJeztah/27.0_backport_bump_engine_27.0.3
[27.0 backport] vendor: github.com/docker/docker v27.0.3
2024-07-03 12:22:56 +02:00
Paweł Gronowski 5ed8f858cf
update to go1.21.12
- https://github.com/golang/go/issues?q=milestone%3AGo1.21.12+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.21.11...go1.21.12

These minor releases include 1 security fixes following the security policy:

net/http: denial of service due to improper 100-continue handling

The net/http HTTP/1.1 client mishandled the case where a server responds to a request with an "Expect: 100-continue" header with a non-informational (200 or higher) status. This mishandling could leave a client connection in an invalid state, where the next request sent on the connection will fail.

An attacker sending a request to a net/http/httputil.ReverseProxy proxy can exploit this mishandling to cause a denial of service by sending "Expect: 100-continue" requests which elicit a non-informational response from the backend. Each such request leaves the proxy with an invalid connection, and causes one subsequent request using that connection to fail.

Thanks to Geoff Franks for reporting this issue.

This is CVE-2024-24791 and Go issue https://go.dev/issue/67555.
View the release notes for more information:
https://go.dev/doc/devel/release#go1.21.12

**- Description for the changelog**

```markdown changelog
Update Go runtime to 1.21.12
```

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit d73d7d4ed3)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-07-03 12:16:54 +02:00
Sebastiaan van Stijn 6b99e0370c
vendor: github.com/docker/docker v27.0.3
full diff: https://github.com/docker/docker/compare/v27.0.2...v27.0.3

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 2e6aaf05d4)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-01 12:30:22 +02:00
Laura Brehm 7d4bcd863a
Merge pull request #5206 from thaJeztah/27.0_backport_docker_27.0.2
[27.0 backport] vendor: github.com/docker/docker v27.0.2
2024-06-28 15:56:30 +01:00
Sebastiaan van Stijn 3134d55821
vendor: github.com/docker/docker v27.0.2
no diff, as it's the same commit tagged: https://github.com/docker/docker/compare/e953d76450b6...v27.0.2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9455d61768)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-06-28 09:08:52 +02:00
Paweł Gronowski 912c1ddf8a
Merge pull request #5202 from vvoland/vendor-docker
[27.0] vendor: github.com/docker/docker v27.0.2-dev (e953d76450b6)
2024-06-26 20:39:48 +02:00
Paweł Gronowski c97e8091a6
vendor: github.com/docker/docker v27.0.2-dev (e953d76450b6)
full diff: 861fde8cc9...e953d76450

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-06-26 20:34:31 +02:00
Laura Brehm 82bd8158f7
Merge pull request #5201 from vvoland/vendor-docker
[27.0] vendor: github.com/docker/docker v27.0.2-dev (861fde8cc974)
2024-06-26 18:58:04 +01:00
Paweł Gronowski 8945848025
vendor: github.com/docker/docker v27.0.2-dev (861fde8cc974)
full diff: https://github.com/docker/docker/compare/v27.0.1...861fde8cc974

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-06-26 19:20:54 +02:00
Sebastiaan van Stijn b54897bcb8
Merge pull request #5199 from vvoland/v27.0-5191
[27.0 backport] gha/e2e: Update latest version to 27.0
2024-06-26 15:45:12 +02:00
Paweł Gronowski cd560916f3
gha/e2e: Update latest version to 27.0
27.0 is out - update the latest version used for e2e and drop the 25.0

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 60775b6150)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-06-26 15:37:10 +02:00
Laura Brehm 9a101a955b
Merge pull request #5198 from thaJeztah/27.0_backport_carry_fix_custom_ports 2024-06-26 14:19:52 +01:00
Sebastiaan van Stijn 50fae20748
cli/config/credentials: ConvertToHostname: handle IP-addresses
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 8b0a7b025d)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-06-26 15:05:38 +02:00
Sebastiaan van Stijn 37533c2f55
Merge pull request #5197 from thaJeztah/27.0_backport_fix_custom_ports
[27.0 backport] re-introduced support for port numbers in docker registry URL
2024-06-26 15:04:51 +02:00
Carston Schilds 217971d481
re-introduced support for port numbers in docker registry URL
Signed-off-by: Carston Schilds <Carston.Schilds@visier.com>
(cherry picked from commit 2380481609)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-06-26 14:13:24 +02:00
Paweł Gronowski fce24d5f8d
Merge pull request #5192 from vvoland/v27.0-5189
[27.0 backport] update golangci-lint to v1.59.1
2024-06-26 13:43:56 +02:00
Paweł Gronowski 0e4f16f3bf
Merge pull request #5190 from vvoland/vendor-docker
[27.0] vendor: github.com/docker/docker v27.0.1
2024-06-25 14:48:44 +02:00
Sebastiaan van Stijn 6e35a78fd9
update golangci-lint to v1.59.1
full diff: https://github.com/golangci/golangci-lint/compare/v1.59.0...v1.59.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit b5d1b4de1a)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-06-25 14:45:13 +02:00
Paweł Gronowski bf1a701820
vendor: github.com/docker/docker v27.0.1
no change in vendored files, just changing a tag

full diff: https://github.com/docker/docker/compare/ff1e2c0de72a...v27.0.1

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-06-25 12:13:00 +02:00
609 changed files with 27974 additions and 9953 deletions

View File

@ -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

View File

@ -1,5 +1,14 @@
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:

View File

@ -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
@ -28,9 +37,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

View File

@ -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
@ -46,7 +55,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 +74,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.21.11
go-version: 1.22.7
-
name: Test
run: |

View File

@ -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
id: title_branch
run: echo "$PR_TITLE" | sed -n 's/^\[\([0-9]*\.[0-9]*\)[^]]*\].*/branch=\1/p' >> $GITHUB_OUTPUT
- 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
id: title_branch
run: |
# get the intended major version prefix ("[27.1 backport]" -> "27.") from the PR title.
[[ "$PR_TITLE" =~ ^\[([0-9]*\.)[^]]*\] ]] && branch="${BASH_REMATCH[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

View File

@ -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

View File

@ -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.7
ARG XX_VERSION=1.5.0
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.16.1
ARG COMPOSE_VERSION=v2.29.0
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx

View File

@ -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

View File

@ -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.

View File

@ -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) {

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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...)
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -2,6 +2,7 @@ package completion
import (
"os"
"strings"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types"
@ -106,6 +107,41 @@ 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

View File

@ -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)
})
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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 {

View File

@ -0,0 +1,94 @@
package container
import (
"github.com/docker/cli/cli/command/completion"
"github.com/docker/docker/api/types/container"
"github.com/moby/sys/signal"
"github.com/spf13/cobra"
)
// allLinuxCapabilities is a list of all known Linux capabilities.
//
// This list was based on the containerd pkg/cap package;
// https://github.com/containerd/containerd/blob/v1.7.19/pkg/cap/cap_linux.go#L133-L181
//
// TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true")
var allLinuxCapabilities = []string{
"ALL", // magic value for "all capabilities"
// caps35 is the caps of kernel 3.5 (37 entries)
"CAP_CHOWN", // 2.2
"CAP_DAC_OVERRIDE", // 2.2
"CAP_DAC_READ_SEARCH", // 2.2
"CAP_FOWNER", // 2.2
"CAP_FSETID", // 2.2
"CAP_KILL", // 2.2
"CAP_SETGID", // 2.2
"CAP_SETUID", // 2.2
"CAP_SETPCAP", // 2.2
"CAP_LINUX_IMMUTABLE", // 2.2
"CAP_NET_BIND_SERVICE", // 2.2
"CAP_NET_BROADCAST", // 2.2
"CAP_NET_ADMIN", // 2.2
"CAP_NET_RAW", // 2.2
"CAP_IPC_LOCK", // 2.2
"CAP_IPC_OWNER", // 2.2
"CAP_SYS_MODULE", // 2.2
"CAP_SYS_RAWIO", // 2.2
"CAP_SYS_CHROOT", // 2.2
"CAP_SYS_PTRACE", // 2.2
"CAP_SYS_PACCT", // 2.2
"CAP_SYS_ADMIN", // 2.2
"CAP_SYS_BOOT", // 2.2
"CAP_SYS_NICE", // 2.2
"CAP_SYS_RESOURCE", // 2.2
"CAP_SYS_TIME", // 2.2
"CAP_SYS_TTY_CONFIG", // 2.2
"CAP_MKNOD", // 2.4
"CAP_LEASE", // 2.4
"CAP_AUDIT_WRITE", // 2.6.11
"CAP_AUDIT_CONTROL", // 2.6.11
"CAP_SETFCAP", // 2.6.24
"CAP_MAC_OVERRIDE", // 2.6.25
"CAP_MAC_ADMIN", // 2.6.25
"CAP_SYSLOG", // 2.6.37
"CAP_WAKE_ALARM", // 3.0
"CAP_BLOCK_SUSPEND", // 3.5
// caps316 is the caps of kernel 3.16 (38 entries)
"CAP_AUDIT_READ",
// caps58 is the caps of kernel 5.8 (40 entries)
"CAP_PERFMON",
"CAP_BPF",
// caps59 is the caps of kernel 5.9 (41 entries)
"CAP_CHECKPOINT_RESTORE",
}
// 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),
}
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)
}

View File

@ -178,13 +178,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)
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(testcase.path)
assert.Check(t, is.Equal(testcase.expectedContainer, ctr))
assert.Check(t, is.Equal(testcase.expectedPath, path))
ctr, path := splitCpArg(tc.path)
assert.Check(t, is.Equal(tc.expectedContainer, ctr))
assert.Check(t, is.Equal(tc.expectedPath, path))
})
}
}

View File

@ -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)
_ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCli))
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
_ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
_ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCli, true))
return cmd
}

View File

@ -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)

View File

@ -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
}

View File

@ -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()

View File

@ -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 {

View File

@ -163,6 +163,7 @@ func TestContainerListErrors(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)
}
}

View File

@ -2,6 +2,7 @@ package container
import (
"context"
"io"
"testing"
"github.com/docker/cli/internal/test"
@ -20,5 +21,7 @@ func TestContainerPrunePromptTermination(t *testing.T) {
},
})
cmd := NewPruneCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
test.TerminatePrompt(ctx, t, cmd, cli)
}

View File

@ -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
}

View File

@ -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()

View File

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"io"
"os"
"strings"
"syscall"
@ -70,22 +69,15 @@ 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("cap-add", completeLinuxCapabilityNames)
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCli))
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
_ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
_ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCli, true))
return cmd
}
@ -119,6 +111,8 @@ func runRun(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, ro
//nolint:gocyclo
func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOptions, copts *containerOptions, containerCfg *containerConfig) error {
ctx = context.WithoutCancel(ctx)
config := containerCfg.Config
stdout, stderr := dockerCli.Out(), dockerCli.Err()
apiClient := dockerCli.Client()
@ -178,6 +172,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,

View File

@ -5,7 +5,6 @@ import (
"errors"
"io"
"net"
"os/signal"
"syscall"
"testing"
"time"
@ -38,15 +37,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 +130,62 @@ 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")
}
}
@ -127,23 +216,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))
})
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -5,6 +5,7 @@ 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

View File

@ -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{
{

View File

@ -15,6 +15,7 @@ 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/image/build"
@ -104,7 +105,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 +115,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 +130,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 +143,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())

View File

@ -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")

View File

@ -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",
},

View File

@ -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)
})
}
}

View File

@ -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())
})
}
}

View File

@ -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()

View File

@ -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)
})
}
}

View File

@ -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,

View File

@ -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))
})
}
}

View File

@ -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))
})
}
}

View File

@ -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)
}

View File

@ -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)
})
}
}

View File

@ -8,7 +8,7 @@ import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"github.com/containerd/platforms"
"github.com/distribution/reference"
@ -58,8 +58,13 @@ 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"})
@ -79,9 +84,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 +184,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...)
}

View File

@ -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 != "" {

View File

@ -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",
},

View File

@ -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()))

View File

@ -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()
}
})
}
}

View File

@ -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)
}
}

393
cli/command/image/tree.go Normal file
View File

@ -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),
Used: 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),
Used: len(im.ImageData.Containers) > 0,
ContentSize: units.HumanSizeWithPrecision(float64(im.Size.Content), 3),
},
}
if sub.Details.Used {
// Mark top-level parent image as used if any of its subimages are used.
details.Used = 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
Used 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.Underline, 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: "Used",
Align: alignCenter,
Width: 4,
Color: &greenColor,
DetailsValue: func(d *imageDetails) string {
if d.Used {
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, "<untagged>"))
}
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
}

View File

@ -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")

View File

@ -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": ""},
}

View File

@ -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")
}

View File

@ -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")

View File

@ -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)
}
}

View File

@ -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")

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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")
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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))
})
}
}

View File

@ -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)
})
}
}

View File

@ -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)
}
}

View File

@ -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")
}

View File

@ -1,10 +1,8 @@
package command
import (
"bufio"
"context"
"fmt"
"io"
"os"
"runtime"
"strings"
@ -18,7 +16,6 @@ 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"
)
@ -44,7 +41,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 +86,32 @@ 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) (authConfig registrytypes.AuthConfig, err error) {
// On Windows, force the use of the regular OS stdin stream.
//
// See:
@ -103,20 +124,10 @@ 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")
}
isDefaultRegistry := serverAddress == registry.IndexServer
defaultUsername = strings.TrimSpace(defaultUsername)
authconfig.Username = strings.TrimSpace(authconfig.Username)
if flUser = strings.TrimSpace(flUser); flUser == "" {
if argUser = strings.TrimSpace(argUser); argUser == "" {
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.")
@ -125,62 +136,45 @@ func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *registrytypes
fmt.Fprintln(cli.Out())
}
}
promptWithDefault(cli.Out(), "Username", authconfig.Username)
var err error
flUser, err = readInput(cli.In())
if err != nil {
return err
var prompt string
if defaultUsername == "" {
prompt = "Username: "
} else {
prompt = fmt.Sprintf("Username (%s): ", defaultUsername)
}
if flUser == "" {
flUser = authconfig.Username
argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
if err != nil {
return authConfig, err
}
if argUser == "" {
argUser = defaultUsername
}
}
if flUser == "" {
return errors.Errorf("Error: Non-null Username Required")
if argUser == "" {
return authConfig, errors.Errorf("Error: Non-null Username Required")
}
if flPassword == "" {
oldState, err := term.SaveState(cli.In().FD())
if argPassword == "" {
restoreInput, err := DisableInputEcho(cli.In())
if err != nil {
return err
return authConfig, err
}
fmt.Fprintf(cli.Out(), "Password: ")
_ = term.DisableEcho(cli.In().FD(), oldState)
defer func() {
_ = term.RestoreTerminal(cli.In().FD(), oldState)
}()
flPassword, err = readInput(cli.In())
defer restoreInput()
argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
if err != nil {
return err
return authConfig, err
}
fmt.Fprint(cli.Out(), "\n")
if flPassword == "" {
return errors.Errorf("Error: Password Required")
if argPassword == "" {
return 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)
}
authConfig.Username = argUser
authConfig.Password = argPassword
authConfig.ServerAddress = serverAddress
return authConfig, nil
}
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete

View File

@ -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 err != nil || authConfig.Username == "" || authConfig.Password == "" {
err = command.ConfigureAuth(dockerCli, opts.user, opts.password, &authConfig, isDefaultRegistry)
if err != nil {
return err
}
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)
}
// If we (still) have an error, give up
// 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 == "" {
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
}
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 response.IdentityToken != "" {
authConfig.Password = ""
authConfig.IdentityToken = response.IdentityToken
}
creds := dockerCli.ConfigFile().GetCredentialsStore(serverAddress)
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 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
}
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)
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)
}
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)
}
return response, err
// If we (still) have an error, give up
if err != nil {
return registrytypes.AuthenticateOKBody{}, err
}
return response, nil
}
func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {

View File

@ -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",
},
},
inputStoredCred: &validAuthConfig,
expectedSavedCred: validAuthConfig,
},
{
doc: "expired auth",
inputLoginOption: loginOptions{
serverAddress: storedServerAddress,
input: loginOptions{
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,
},
inputStoredCred: &validAuthConfig,
expectedSavedCred: configtypes.AuthConfig{
ServerAddress: storedServerAddress,
Username: validUsername,
Password: validPassword2,
expectedCredentials: map[string]configtypes.AuthConfig{
"reg1": {
Username: "my-username",
Password: "a-password",
ServerAddress: "reg1",
},
},
},
{
doc: "unknown user",
inputLoginOption: loginOptions{
serverAddress: storedServerAddress,
doc: "expired auth from store",
priorCredentials: map[string]configtypes.AuthConfig{
"reg1": {
Username: "my-username",
Password: expiredPassword,
ServerAddress: "reg1",
},
},
input: loginOptions{
serverAddress: "reg1",
},
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
},
{
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",
},
},
},
{
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: validPassword,
password: "a-password",
},
expectedErr: errUnknownUser,
expectedCredentials: map[string]configtypes.AuthConfig{
"reg1": {
Username: "a-password",
Password: "a-password",
ServerAddress: "reg1",
},
},
inputStoredCred: &validAuthConfig,
expectedErr: errUnknownUser,
},
{
doc: "valid token",
inputLoginOption: loginOptions{
serverAddress: storedServerAddress,
user: validUsername,
doc: "unknown user w/o prior credentials",
priorCredentials: map[string]configtypes.AuthConfig{},
input: loginOptions{
serverAddress: "reg1",
user: unknownUser,
password: "a-password",
},
expectedErr: errUnknownUser,
expectedCredentials: map[string]configtypes.AuthConfig{},
},
{
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)
}
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -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))

View File

@ -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"})

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
})
}
}

View File

@ -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 {

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