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
2024 changed files with 56560 additions and 91894 deletions

View File

@ -1,6 +1,6 @@
/build/ /build/
/cmd/docker/winresources/versioninfo.json /cli/winresources/versioninfo.json
/cmd/docker/winresources/*.syso /cli/winresources/*.syso
/man/man*/ /man/man*/
/man/vendor/ /man/vendor/
/man/go.sum /man/go.sum

11
.gitattributes vendored
View File

@ -1,14 +1,3 @@
* text=auto
Dockerfile* linguist-language=Dockerfile Dockerfile* linguist-language=Dockerfile
vendor.mod linguist-language=Go-Module vendor.mod linguist-language=Go-Module
vendor.sum linguist-language=Go-Checksums vendor.sum linguist-language=Go-Checksums
*.go -text diff=golang
# scripts directory contains shell scripts
# without extensions, so we need to force
scripts/** text=auto eol=lf
# shell scripts should always have LF
*.sh text eol=lf

View File

@ -19,14 +19,11 @@ Provide the following information:
**- How to verify it** **- How to verify it**
**- Human readable description for the release notes** **- Description for the changelog**
<!-- <!--
Write a short (one line) summary that describes the changes in this Write a short (one line) summary that describes the changes in this
pull request for inclusion in the changelog. pull request for inclusion in the changelog.
It must be placed inside the below triple backticks section. It must be placed inside the below triple backticks section:
NOTE: Only fill this section if changes introduced in this PR are user-facing.
The PR must have a relevant impact/ label.
--> -->
```markdown changelog ```markdown changelog

View File

@ -22,7 +22,6 @@ on:
branches: branches:
- 'master' - 'master'
- '[0-9]+.[0-9]+' - '[0-9]+.[0-9]+'
- '[0-9]+.x'
tags: tags:
- 'v*' - 'v*'
pull_request: pull_request:
@ -61,12 +60,17 @@ jobs:
- "" - ""
- glibc - glibc
steps: steps:
-
name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- -
name: Build name: Build
uses: docker/bake-action@v6 uses: docker/bake-action@v5
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
set: | set: |
@ -99,12 +103,8 @@ jobs:
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }} if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
steps: steps:
- -
name: Login to DockerHub name: Checkout
if: github.event_name != 'pull_request' uses: actions/checkout@v4
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_CLIBIN_USERNAME }}
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
- -
name: Set up QEMU name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
@ -121,15 +121,21 @@ jobs:
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=ref,event=branch type=ref,event=branch
type=ref,event=pr type=ref,event=pr
type=semver,pattern={{major}} type=sha
type=semver,pattern={{major}}.{{minor}} -
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_CLIBIN_USERNAME }}
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
- -
name: Build and push image name: Build and push image
uses: docker/bake-action@v6 uses: docker/bake-action@v5
with: with:
files: | files: |
./docker-bake.hcl ./docker-bake.hcl
cwd://${{ steps.meta.outputs.bake-file }} ${{ steps.meta.outputs.bake-file }}
targets: bin-image-cross targets: bin-image-cross
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
set: | set: |
@ -163,12 +169,15 @@ jobs:
matrix: matrix:
platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }} platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }}
steps: steps:
-
name: Checkout
uses: actions/checkout@v4
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- -
name: Build name: Build
uses: docker/bake-action@v6 uses: docker/bake-action@v5
with: with:
targets: plugins-cross targets: plugins-cross
set: | set: |

View File

@ -14,7 +14,6 @@ on:
branches: branches:
- 'master' - 'master'
- '[0-9]+.[0-9]+' - '[0-9]+.[0-9]+'
- '[0-9]+.x'
tags: tags:
- 'v*' - 'v*'
pull_request: pull_request:
@ -34,8 +33,8 @@ on:
jobs: jobs:
codeql: codeql:
runs-on: ubuntu-24.04 runs-on: 'ubuntu-latest'
timeout-minutes: 10 timeout-minutes: 360
env: env:
DISABLE_WARN_OUTSIDE_CONTAINER: '1' DISABLE_WARN_OUTSIDE_CONTAINER: '1'
permissions: permissions:
@ -49,6 +48,11 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 2 fetch-depth: 2
-
name: Checkout HEAD on PR
if: ${{ github.event_name == 'pull_request' }}
run: |
git checkout HEAD^2
# CodeQL 2.16.4's auto-build added support for multi-module repositories, # CodeQL 2.16.4's auto-build added support for multi-module repositories,
# and is trying to be smart by searching for modules in every directory, # and is trying to be smart by searching for modules in every directory,
# including vendor directories. If no module is found, it's creating one # including vendor directories. If no module is found, it's creating one
@ -63,7 +67,7 @@ jobs:
name: Update Go name: Update Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: "1.24.5" go-version: '1.21'
- -
name: Initialize CodeQL name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v3

View File

@ -19,28 +19,29 @@ on:
branches: branches:
- 'master' - 'master'
- '[0-9]+.[0-9]+' - '[0-9]+.[0-9]+'
- '[0-9]+.x'
tags: tags:
- 'v*' - 'v*'
pull_request: pull_request:
jobs: jobs:
tests: e2e:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
target: target:
- local - non-experimental
- experimental
- connhelper-ssh - connhelper-ssh
base: base:
- alpine - alpine
- debian - debian
engine-version: engine-version:
- 28 # latest - 27.0 # latest
- 27 # latest - 1 - 26.1 # latest - 1
- 26 # github actions default - 23.0 # mirantis lts
- 23 # 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
steps: steps:
- -
name: Checkout name: Checkout
@ -74,7 +75,7 @@ jobs:
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
- -
name: Send to Codecov name: Send to Codecov
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v4
with: with:
files: ./build/coverage/coverage.txt file: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -19,7 +19,6 @@ on:
branches: branches:
- 'master' - 'master'
- '[0-9]+.[0-9]+' - '[0-9]+.[0-9]+'
- '[0-9]+.x'
tags: tags:
- 'v*' - 'v*'
pull_request: pull_request:
@ -28,19 +27,22 @@ jobs:
ctn: ctn:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
-
name: Checkout
uses: actions/checkout@v4
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- -
name: Test name: Test
uses: docker/bake-action@v6 uses: docker/bake-action@v5
with: with:
targets: test-coverage targets: test-coverage
- -
name: Send to Codecov name: Send to Codecov
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v4
with: with:
files: ./build/coverage/coverage.txt file: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
host: host:
@ -57,6 +59,12 @@ jobs:
- macos-14 # macOS 14 on arm64 (Apple Silicon M1) - 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 # - 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: steps:
-
name: Prepare git
if: matrix.os == 'windows-latest'
run: |
git config --system core.autocrlf false
git config --system core.eol lf
- -
name: Checkout name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -66,7 +74,7 @@ jobs:
name: Set up Go name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: "1.24.5" go-version: 1.22.7
- -
name: Test name: Test
run: | run: |
@ -76,8 +84,8 @@ jobs:
shell: bash shell: bash
- -
name: Send to Codecov name: Send to Codecov
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v4
with: with:
files: /tmp/coverage.txt file: /tmp/coverage.txt
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -15,8 +15,7 @@ on:
jobs: jobs:
check-area-label: check-area-label:
runs-on: ubuntu-24.04 runs-on: ubuntu-20.04
timeout-minutes: 120 # guardrails timeout for the whole job
steps: steps:
- name: Missing `area/` label - name: Missing `area/` label
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/') if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
@ -27,10 +26,9 @@ jobs:
run: exit 0 run: exit 0
check-changelog: check-changelog:
runs-on: ubuntu-24.04 if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/')
timeout-minutes: 120 # guardrails timeout for the whole job runs-on: ubuntu-20.04
env: env:
HAS_IMPACT_LABEL: ${{ contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') }}
PR_BODY: | PR_BODY: |
${{ github.event.pull_request.body }} ${{ github.event.pull_request.body }}
steps: steps:
@ -42,9 +40,8 @@ jobs:
# Strip empty lines # Strip empty lines
desc=$(echo "$block" | awk NF) desc=$(echo "$block" | awk NF)
if [ "$HAS_IMPACT_LABEL" = "true" ]; then
if [ -z "$desc" ]; then if [ -z "$desc" ]; then
echo "::error::Changelog section is empty. Please provide a description for the changelog." echo "::error::Changelog section is empty. Provide a description for the changelog."
exit 1 exit 1
fi fi
@ -53,20 +50,12 @@ jobs:
echo "::error::Description looks too short: $desc" echo "::error::Description looks too short: $desc"
exit 1 exit 1
fi fi
else
if [ -n "$desc" ]; then
echo "::error::PR has a changelog description, but no changelog label"
echo "::error::Please add the relevant 'impact/' label to the PR or remove the changelog description"
exit 1
fi
fi
echo "This PR will be included in the release notes with the following note:" echo "This PR will be included in the release notes with the following note:"
echo "$desc" echo "$desc"
check-pr-branch: check-pr-branch:
runs-on: ubuntu-24.04 runs-on: ubuntu-20.04
timeout-minutes: 120 # guardrails timeout for the whole job
env: env:
PR_TITLE: ${{ github.event.pull_request.title }} PR_TITLE: ${{ github.event.pull_request.title }}
steps: steps:

View File

@ -19,7 +19,6 @@ on:
branches: branches:
- 'master' - 'master'
- '[0-9]+.[0-9]+' - '[0-9]+.[0-9]+'
- '[0-9]+.x'
tags: tags:
- 'v*' - 'v*'
pull_request: pull_request:
@ -36,9 +35,12 @@ jobs:
- validate-vendor - validate-vendor
- update-authors # ensure authors update target runs fine - update-authors # ensure authors update target runs fine
steps: steps:
-
name: Checkout
uses: actions/checkout@v4
- -
name: Run name: Run
uses: docker/bake-action@v6 uses: docker/bake-action@v5
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}

4
.gitignore vendored
View File

@ -8,8 +8,8 @@
Thumbs.db Thumbs.db
.editorconfig .editorconfig
/build/ /build/
/cmd/docker/winresources/versioninfo.json /cli/winresources/versioninfo.json
/cmd/docker/winresources/*.syso /cli/winresources/*.syso
profile.out profile.out
# top-level go.mod is not meant to be checked in # top-level go.mod is not meant to be checked in

View File

@ -1,226 +1,187 @@
version: "2"
run:
# prevent golangci-lint from deducting the go version to lint for through go.mod,
# which causes it to fallback to go1.17 semantics.
#
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
go: "1.24.5"
timeout: 5m
issues:
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-issues-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0
formatters:
enable:
- gofumpt # Detects whether code was gofumpt-ed.
- goimports
exclusions:
generated: strict
linters: linters:
enable: enable:
- asasalint # Detects "[]any" used as argument for variadic "func(...any)".
- bodyclose - bodyclose
- copyloopvar # Detects places where loop variables are copied.
- depguard - depguard
- dogsled # Detects assignments with too many blank identifiers. - dogsled
- dupword # Detects duplicate words. - dupword # Detects duplicate words.
- durationcheck # Detect cases where two time.Duration values are being multiplied in possibly erroneous ways. - durationcheck
- errcheck - errchkjson
- errchkjson # Detects unsupported types passed to json encoding functions and reports if checks for the returned error can be omitted. - exportloopref # Detects pointers to enclosing loop variables.
- exhaustive # Detects missing options in enum switch statements.
- exptostd # Detects functions from golang.org/x/exp/ that can be replaced by std functions.
- fatcontext # Detects nested contexts in loops and function literals.
- forbidigo
- gocheckcompilerdirectives # Detects invalid go compiler directive comments (//go:).
- gocritic # Metalinter; detects bugs, performance, and styling issues. - gocritic # Metalinter; detects bugs, performance, and styling issues.
- gocyclo - gocyclo
- gofumpt # Detects whether code was gofumpt-ed.
- goimports
- gosec # Detects security problems. - gosec # Detects security problems.
- gosimple
- govet - govet
- iface # Detects incorrect use of interfaces. Currently only used for "identical" interfaces in the same package.
- importas # Enforces consistent import aliases.
- ineffassign - ineffassign
- makezero # Finds slice declarations with non-zero initial length. - lll
- mirror # Detects wrong mirror patterns of bytes/strings usage. - megacheck
- misspell # Detects commonly misspelled English words in comments. - misspell # Detects commonly misspelled English words in comments.
- nakedret # Detects uses of naked returns. - nakedret
- nilnesserr # Detects returning nil errors. It combines the features of nilness and nilerr, - nilerr # Detects code that returns nil even if it checks that the error is not nil.
- nosprintfhostport # Detects misuse of Sprintf to construct a host with port in a URL.
- nolintlint # Detects ill-formed or insufficient nolint directives. - nolintlint # Detects ill-formed or insufficient nolint directives.
- perfsprint # Detects fmt.Sprintf uses that can be replaced with a faster alternative. - perfsprint # Detects fmt.Sprintf uses that can be replaced with a faster alternative.
- prealloc # Detects slice declarations that could potentially be pre-allocated. - prealloc # Detects slice declarations that could potentially be pre-allocated.
- predeclared # Detects code that shadows one of Go's predeclared identifiers - predeclared # Detects code that shadows one of Go's predeclared identifiers
- reassign # Detects reassigning a top-level variable in another package. - reassign
- revive # Metalinter; drop-in replacement for golint. - revive # Metalinter; drop-in replacement for golint.
- spancheck # Detects mistakes with OpenTelemetry/Census spans.
- staticcheck - staticcheck
- stylecheck # Replacement for golint
- tenv # Detects using os.Setenv instead of t.Setenv.
- thelper # Detects test helpers without t.Helper(). - thelper # Detects test helpers without t.Helper().
- tparallel # Detects inappropriate usage of t.Parallel(). - tparallel # Detects inappropriate usage of t.Parallel().
- typecheck
- unconvert # Detects unnecessary type conversions. - unconvert # Detects unnecessary type conversions.
- unparam - unparam
- unused - unused
- usestdlibvars # Detects the possibility to use variables/constants from the Go standard library. - usestdlibvars
- usetesting # Reports uses of functions with replacement inside the testing package. - vet
- wastedassign # Detects wasted assignment statements. - wastedassign
disable: disable:
- errcheck - errcheck
settings: run:
timeout: 5m
linters-settings:
depguard: depguard:
rules: rules:
main: main:
deny: deny:
- pkg: "github.com/containerd/containerd/errdefs" - pkg: io/ioutil
desc: The containerd errdefs package was migrated to a separate module. Use github.com/containerd/errdefs instead.
- pkg: "github.com/containerd/containerd/log"
desc: The containerd log package was migrated to a separate module. Use github.com/containerd/log instead.
- pkg: "github.com/containerd/containerd/pkg/userns"
desc: Use github.com/moby/sys/userns instead.
- pkg: "github.com/containerd/containerd/platforms"
desc: The containerd platforms package was migrated to a separate module. Use github.com/containerd/platforms instead.
- pkg: "github.com/docker/docker/pkg/system"
desc: This package should not be used unless strictly necessary.
- pkg: "github.com/docker/distribution/uuid"
desc: Use github.com/google/uuid instead.
- pkg: "io/ioutil"
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
forbidigo:
forbid:
- pkg: ^regexp$
pattern: ^regexp\.MustCompile
msg: Use internal/lazyregexp.New instead.
gocyclo: gocyclo:
min-complexity: 16 min-complexity: 16
gosec:
excludes:
- G104 # G104: Errors unhandled; (TODO: reduce unhandled errors, or explicitly ignore)
- G115 # G115: integer overflow conversion; (TODO: verify these: https://github.com/docker/cli/issues/5584)
- G306 # G306: Expect WriteFile permissions to be 0600 or less (too restrictive; also flags "0o644" permissions)
- G307 # G307: Deferring unsafe method "*os.File" on type "Close" (also EXC0008); (TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close")
govet: govet:
enable: enable:
- shadow - shadow
settings: settings:
shadow: shadow:
strict: true strict: true
lll: lll:
line-length: 200 line-length: 200
importas:
# Do not allow unaliased imports of aliased packages.
no-unaliased: true
alias:
# Enforce alias to prevent it accidentally being used instead of our
# own errdefs package (or vice-versa).
- pkg: github.com/containerd/errdefs
alias: cerrdefs
- pkg: github.com/opencontainers/image-spec/specs-go/v1
alias: ocispec
# Enforce that gotest.tools/v3/assert/cmp is always aliased as "is"
- pkg: gotest.tools/v3/assert/cmp
alias: is
nakedret: nakedret:
# Disallow naked returns if func has more lines of code than this setting. command: nakedret
# Default: 30 pattern: ^(?P<path>.*?\\.go):(?P<line>\\d+)\\s*(?P<message>.*)$
max-func-lines: 0
staticcheck:
checks:
- all
- -QF1008 # Omit embedded fields from selector expression; https://staticcheck.dev/docs/checks/#QF1008
revive: revive:
rules: rules:
- name: empty-block # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing
- name: empty-lines # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines - name: import-shadowing
- name: import-shadowing # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing severity: warning
- name: line-length-limit # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#line-length-limit disabled: false
arguments: [200] # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block
- name: unused-receiver # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver - name: empty-block
- name: use-any # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
- name: empty-lines
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
- name: use-any
severity: warning
disabled: false
usetesting: issues:
os-chdir: false # FIXME(thaJeztah): Disable `os.Chdir()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478 # The default exclusion rules are a bit too permissive, so copying the relevant ones below
context-background: false # FIXME(thaJeztah): Disable `context.Background()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478 exclude-use-default: false
context-todo: false # FIXME(thaJeztah): Disable `context.TODO()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
exclusions: exclude:
# We prefer to use an "linters.exclusions.rules" so that new "default" exclusions are not - parameter .* always receives
exclude-files:
- cli/compose/schema/bindata.go
- .*generated.*
exclude-rules:
# We prefer to use an "exclude-list" so that new "default" exclusions are not
# automatically inherited. We can decide whether or not to follow upstream # automatically inherited. We can decide whether or not to follow upstream
# defaults when updating golang-ci-lint versions. # defaults when updating golang-ci-lint versions.
# Unfortunately, this means we have to copy the whole exclusion pattern, as # Unfortunately, this means we have to copy the whole exclusion pattern, as
# (unlike the "include" option), the "exclude" option does not take exclusion # (unlike the "include" option), the "exclude" option does not take exclusion
# ID's. # ID's.
# #
# These exclusion patterns are copied from the default excludes at: # These exclusion patterns are copied from the default excluses at:
# https://github.com/golangci/golangci-lint/blob/v1.61.0/pkg/config/issues.go#L11-L104 # https://github.com/golangci/golangci-lint/blob/v1.44.0/pkg/config/issues.go#L10-L104
#
# The default list of exclusions can be found at:
# https://golangci-lint.run/usage/false-positives/#default-exclusions
generated: strict
rules: # EXC0001
- text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv). is not checked"
linters:
- errcheck
# EXC0003 # EXC0003
- text: "func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this" - text: "func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this"
linters: linters:
- revive - revive
# EXC0006
- text: "Use of unsafe calls should be audited"
linters:
- gosec
# EXC0007 # EXC0007
- text: "Subprocess launch(ed with variable|ing should be audited)" - text: "Subprocess launch(ed with variable|ing should be audited)"
linters: linters:
- gosec - gosec
# EXC0008
# TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close" (gosec)
- text: "G307"
linters:
- gosec
# EXC0009 # EXC0009
- text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)" - text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
linters: linters:
- gosec - gosec
# EXC0010 # EXC0010
- text: "Potential file inclusion via variable" - text: "Potential file inclusion via variable"
linters: linters:
- gosec - gosec
# G113 Potential uncontrolled memory consumption in Rat.SetString (CVE-2022-23772)
# only affects gp < 1.16.14. and go < 1.17.7
- text: "G113"
linters:
- gosec
# TODO: G104: Errors unhandled. (gosec)
- text: "G104"
linters:
- gosec
# Looks like the match in "EXC0007" above doesn't catch this one
# TODO: consider upstreaming this to golangci-lint's default exclusion rules
- text: "G204: Subprocess launched with a potential tainted input or cmd arguments"
linters:
- gosec
# Looks like the match in "EXC0009" above doesn't catch this one
# TODO: consider upstreaming this to golangci-lint's default exclusion rules
- text: "G306: Expect WriteFile permissions to be 0600 or less"
linters:
- gosec
# TODO: make sure all packages have a description. Currently, there's 67 packages without. # TODO: make sure all packages have a description. Currently, there's 67 packages without.
- text: "package-comments: should have a package comment" - text: "package-comments: should have a package comment"
linters: linters:
- revive - revive
# FIXME temporarily suppress these (see https://github.com/gotestyourself/gotest.tools/issues/272)
- text: "SA1019: (assert|cmp|is)\\.ErrorType is deprecated"
linters:
- staticcheck
# Exclude some linters from running on tests files. # Exclude some linters from running on tests files.
- path: _test\.go - path: _test\.go
linters: linters:
- errcheck - errcheck
- gosec - gosec
- text: "ST1000: at least one file in a package should have a package comment" - text: "ST1000: at least one file in a package should have a package comment"
linters: linters:
- staticcheck - stylecheck
# Allow "err" and "ok" vars to shadow existing declarations, otherwise we get too many false positives. # Allow "err" and "ok" vars to shadow existing declarations, otherwise we get too many false positives.
- text: '^shadow: declaration of "(err|ok)" shadows declaration' - text: '^shadow: declaration of "(err|ok)" shadows declaration'
linters: linters:
- govet - govet
# Ignore for cli/command/formatter/tabwriter, which is forked from go stdlib, so we want to align with it.
- text: '^(ST1020|ST1022): comment on exported'
path: "cli/command/formatter/tabwriter"
linters:
- staticcheck
# Log a warning if an exclusion rule is unused. # Maximum issues count per one linter. Set to 0 to disable. Default is 50.
# Default: false max-issues-per-linter: 0
warn-unused: true
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0

View File

@ -43,7 +43,6 @@ Alexis Couvreur <alexiscouvreur.pro@gmail.com>
Alicia Lauerman <alicia@eta.im> <allydevour@me.com> Alicia Lauerman <alicia@eta.im> <allydevour@me.com>
Allen Sun <allensun.shl@alibaba-inc.com> <allen.sun@daocloud.io> Allen Sun <allensun.shl@alibaba-inc.com> <allen.sun@daocloud.io>
Allen Sun <allensun.shl@alibaba-inc.com> <shlallen1990@gmail.com> Allen Sun <allensun.shl@alibaba-inc.com> <shlallen1990@gmail.com>
Allie Sadler <allie.sadler@docker.com>
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@microsoft.com> Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@microsoft.com>
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@outlook.com> Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@outlook.com>
André Martins <aanm90@gmail.com> <martins@noironetworks.com> André Martins <aanm90@gmail.com> <martins@noironetworks.com>
@ -66,9 +65,6 @@ Arnaud Porterie <icecrime@gmail.com>
Arnaud Porterie <icecrime@gmail.com> <arnaud.porterie@docker.com> Arnaud Porterie <icecrime@gmail.com> <arnaud.porterie@docker.com>
Arthur Gautier <baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net> Arthur Gautier <baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net>
Arthur Peka <arthur.peka@outlook.com> <arthrp@users.noreply.github.com> Arthur Peka <arthur.peka@outlook.com> <arthrp@users.noreply.github.com>
Austin Vazquez <austin.vazquez.dev@gmail.com>
Austin Vazquez <austin.vazquez.dev@gmail.com> <55906459+austinvazquez@users.noreply.github.com>
Austin Vazquez <austin.vazquez.dev@gmail.com> <macedonv@amazon.com>
Avi Miller <avi.miller@oracle.com> <avi.miller@gmail.com> Avi Miller <avi.miller@oracle.com> <avi.miller@gmail.com>
Ben Bonnefoy <frenchben@docker.com> Ben Bonnefoy <frenchben@docker.com>
Ben Golub <ben.golub@dotcloud.com> Ben Golub <ben.golub@dotcloud.com>
@ -199,8 +195,6 @@ Gaetan de Villele <gdevillele@gmail.com>
Gang Qiao <qiaohai8866@gmail.com> <1373319223@qq.com> Gang Qiao <qiaohai8866@gmail.com> <1373319223@qq.com>
George Kontridze <george@bugsnag.com> George Kontridze <george@bugsnag.com>
Gerwim Feiken <g.feiken@tfe.nl> <gerwim@gmail.com> Gerwim Feiken <g.feiken@tfe.nl> <gerwim@gmail.com>
Giau. Tran Minh <hello@giautm.dev>
Giau. Tran Minh <hello@giautm.dev> <12751435+giautm@users.noreply.github.com>
Giampaolo Mancini <giampaolo@trampolineup.com> Giampaolo Mancini <giampaolo@trampolineup.com>
Gopikannan Venugopalsamy <gopikannan.venugopalsamy@gmail.com> Gopikannan Venugopalsamy <gopikannan.venugopalsamy@gmail.com>
Gou Rao <gou@portworx.com> <gourao@users.noreply.github.com> Gou Rao <gou@portworx.com> <gourao@users.noreply.github.com>
@ -220,7 +214,6 @@ Günther Jungbluth <gunther@gameslabs.net>
Hakan Özler <hakan.ozler@kodcu.com> Hakan Özler <hakan.ozler@kodcu.com>
Hao Shu Wei <haosw@cn.ibm.com> Hao Shu Wei <haosw@cn.ibm.com>
Hao Shu Wei <haosw@cn.ibm.com> <haoshuwei1989@163.com> Hao Shu Wei <haosw@cn.ibm.com> <haoshuwei1989@163.com>
Harald Albers <github@albersweb.de>
Harald Albers <github@albersweb.de> <albers@users.noreply.github.com> Harald Albers <github@albersweb.de> <albers@users.noreply.github.com>
Harold Cooper <hrldcpr@gmail.com> Harold Cooper <hrldcpr@gmail.com>
Harry Zhang <harryz@hyper.sh> <harryzhang@zju.edu.cn> Harry Zhang <harryz@hyper.sh> <harryzhang@zju.edu.cn>
@ -337,8 +330,6 @@ Lajos Papp <lajos.papp@sequenceiq.com> <lalyos@yahoo.com>
Lei Jitang <leijitang@huawei.com> Lei Jitang <leijitang@huawei.com>
Lei Jitang <leijitang@huawei.com> <leijitang@gmail.com> Lei Jitang <leijitang@huawei.com> <leijitang@gmail.com>
Li Fu Bang <lifubang@acmcoder.com> Li Fu Bang <lifubang@acmcoder.com>
Li Yi <denverdino@gmail.com>
Li Yi <denverdino@gmail.com> <weiyuan.yl@alibaba-inc.com>
Liang Mingqiang <mqliang.zju@gmail.com> Liang Mingqiang <mqliang.zju@gmail.com>
Liang-Chi Hsieh <viirya@gmail.com> Liang-Chi Hsieh <viirya@gmail.com>
Liao Qingwei <liaoqingwei@huawei.com> Liao Qingwei <liaoqingwei@huawei.com>
@ -348,7 +339,6 @@ Lokesh Mandvekar <lsm5@fedoraproject.org> <lsm5@redhat.com>
Lorenzo Fontana <lo@linux.com> <fontanalorenzo@me.com> Lorenzo Fontana <lo@linux.com> <fontanalorenzo@me.com>
Louis Opter <kalessin@kalessin.fr> Louis Opter <kalessin@kalessin.fr>
Louis Opter <kalessin@kalessin.fr> <louis@dotcloud.com> Louis Opter <kalessin@kalessin.fr> <louis@dotcloud.com>
Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
Luca Favatella <luca.favatella@erlang-solutions.com> <lucafavatella@users.noreply.github.com> Luca Favatella <luca.favatella@erlang-solutions.com> <lucafavatella@users.noreply.github.com>
Luke Marsden <me@lukemarsden.net> <luke@digital-crocus.com> Luke Marsden <me@lukemarsden.net> <luke@digital-crocus.com>
Lyn <energylyn@zju.edu.cn> Lyn <energylyn@zju.edu.cn>
@ -406,7 +396,6 @@ Mike Dalton <mikedalton@github.com> <19153140+mikedalton@users.noreply.github.co
Mike Goelzer <mike.goelzer@docker.com> <mgoelzer@docker.com> Mike Goelzer <mike.goelzer@docker.com> <mgoelzer@docker.com>
Milind Chawre <milindchawre@gmail.com> Milind Chawre <milindchawre@gmail.com>
Misty Stanley-Jones <misty@docker.com> <misty@apache.org> Misty Stanley-Jones <misty@docker.com> <misty@apache.org>
Mohammad Hossein <mhm98035@gmail.com>
Mohit Soni <mosoni@ebay.com> <mohitsoni1989@gmail.com> Mohit Soni <mosoni@ebay.com> <mohitsoni1989@gmail.com>
Moorthy RS <rsmoorthy@gmail.com> <rsmoorthy@users.noreply.github.com> Moorthy RS <rsmoorthy@gmail.com> <rsmoorthy@users.noreply.github.com>
Morten Hekkvang <morten.hekkvang@sbab.se> Morten Hekkvang <morten.hekkvang@sbab.se>
@ -430,7 +419,6 @@ O.S. Tezer <ostezer@gmail.com> <ostezer@users.noreply.github.com>
Oh Jinkyun <tintypemolly@gmail.com> <tintypemolly@Ohui-MacBook-Pro.local> Oh Jinkyun <tintypemolly@gmail.com> <tintypemolly@Ohui-MacBook-Pro.local>
Oliver Pomeroy <oppomeroy@gmail.com> Oliver Pomeroy <oppomeroy@gmail.com>
Ouyang Liduo <oyld0210@163.com> Ouyang Liduo <oyld0210@163.com>
Patrick St. laurent <patrick@saint-laurent.us>
Patrick Stapleton <github@gdi2290.com> Patrick Stapleton <github@gdi2290.com>
Paul Liljenberg <liljenberg.paul@gmail.com> <letters@paulnotcom.se> Paul Liljenberg <liljenberg.paul@gmail.com> <letters@paulnotcom.se>
Pavel Tikhomirov <ptikhomirov@virtuozzo.com> <ptikhomirov@parallels.com> Pavel Tikhomirov <ptikhomirov@virtuozzo.com> <ptikhomirov@parallels.com>
@ -510,7 +498,6 @@ Stephen Day <stevvooe@gmail.com> <stephen.day@docker.com>
Stephen Day <stevvooe@gmail.com> <stevvooe@users.noreply.github.com> Stephen Day <stevvooe@gmail.com> <stevvooe@users.noreply.github.com>
Steve Desmond <steve@vtsv.ca> <stevedesmond-ca@users.noreply.github.com> Steve Desmond <steve@vtsv.ca> <stevedesmond-ca@users.noreply.github.com>
Steve Richards <steve.richards@docker.com> stevejr <> Steve Richards <steve.richards@docker.com> stevejr <>
Stuart Williams <pid@pidster.com>
Sun Gengze <690388648@qq.com> Sun Gengze <690388648@qq.com>
Sun Jianbo <wonderflow.sun@gmail.com> Sun Jianbo <wonderflow.sun@gmail.com>
Sun Jianbo <wonderflow.sun@gmail.com> <wonderflow@zju.edu.cn> Sun Jianbo <wonderflow.sun@gmail.com> <wonderflow@zju.edu.cn>

31
AUTHORS
View File

@ -48,7 +48,6 @@ Alfred Landrum <alfred.landrum@docker.com>
Ali Rostami <rostami.ali@gmail.com> Ali Rostami <rostami.ali@gmail.com>
Alicia Lauerman <alicia@eta.im> Alicia Lauerman <alicia@eta.im>
Allen Sun <allensun.shl@alibaba-inc.com> Allen Sun <allensun.shl@alibaba-inc.com>
Allie Sadler <allie.sadler@docker.com>
Alvin Deng <alvin.q.deng@utexas.edu> Alvin Deng <alvin.q.deng@utexas.edu>
Amen Belayneh <amenbelayneh@gmail.com> Amen Belayneh <amenbelayneh@gmail.com>
Amey Shrivastava <72866602+AmeyShrivastava@users.noreply.github.com> Amey Shrivastava <72866602+AmeyShrivastava@users.noreply.github.com>
@ -82,7 +81,6 @@ Antonis Kalipetis <akalipetis@gmail.com>
Anusha Ragunathan <anusha.ragunathan@docker.com> Anusha Ragunathan <anusha.ragunathan@docker.com>
Ao Li <la9249@163.com> Ao Li <la9249@163.com>
Arash Deshmeh <adeshmeh@ca.ibm.com> Arash Deshmeh <adeshmeh@ca.ibm.com>
Archimedes Trajano <developer@trajano.net>
Arko Dasgupta <arko@tetrate.io> Arko Dasgupta <arko@tetrate.io>
Arnaud Porterie <icecrime@gmail.com> Arnaud Porterie <icecrime@gmail.com>
Arnaud Rebillout <elboulangero@gmail.com> Arnaud Rebillout <elboulangero@gmail.com>
@ -90,7 +88,6 @@ Arthur Peka <arthur.peka@outlook.com>
Ashly Mathew <ashly.mathew@sap.com> Ashly Mathew <ashly.mathew@sap.com>
Ashwini Oruganti <ashwini.oruganti@gmail.com> Ashwini Oruganti <ashwini.oruganti@gmail.com>
Aslam Ahemad <aslamahemad@gmail.com> Aslam Ahemad <aslamahemad@gmail.com>
Austin Vazquez <austin.vazquez.dev@gmail.com>
Azat Khuyiyakhmetov <shadow_uz@mail.ru> Azat Khuyiyakhmetov <shadow_uz@mail.ru>
Bardia Keyoumarsi <bkeyouma@ucsc.edu> Bardia Keyoumarsi <bkeyouma@ucsc.edu>
Barnaby Gray <barnaby@pickle.me.uk> Barnaby Gray <barnaby@pickle.me.uk>
@ -135,7 +132,6 @@ Cao Weiwei <cao.weiwei30@zte.com.cn>
Carlo Mion <mion00@gmail.com> Carlo Mion <mion00@gmail.com>
Carlos Alexandro Becker <caarlos0@gmail.com> Carlos Alexandro Becker <caarlos0@gmail.com>
Carlos de Paula <me@carlosedp.com> Carlos de Paula <me@carlosedp.com>
Carston Schilds <Carston.Schilds@visier.com>
Casey Korver <casey@korver.dev> Casey Korver <casey@korver.dev>
Ce Gao <ce.gao@outlook.com> Ce Gao <ce.gao@outlook.com>
Cedric Davies <cedricda@microsoft.com> Cedric Davies <cedricda@microsoft.com>
@ -193,7 +189,6 @@ Daisuke Ito <itodaisuke00@gmail.com>
dalanlan <dalanlan925@gmail.com> dalanlan <dalanlan925@gmail.com>
Damien Nadé <github@livna.org> Damien Nadé <github@livna.org>
Dan Cotora <dan@bluevision.ro> Dan Cotora <dan@bluevision.ro>
Dan Wallis <dan@wallis.nz>
Danial Gharib <danial.mail.gh@gmail.com> Danial Gharib <danial.mail.gh@gmail.com>
Daniel Artine <daniel.artine@ufrj.br> Daniel Artine <daniel.artine@ufrj.br>
Daniel Cassidy <mail@danielcassidy.me.uk> Daniel Cassidy <mail@danielcassidy.me.uk>
@ -242,7 +237,6 @@ Deshi Xiao <dxiao@redhat.com>
Dharmit Shah <shahdharmit@gmail.com> Dharmit Shah <shahdharmit@gmail.com>
Dhawal Yogesh Bhanushali <dbhanushali@vmware.com> Dhawal Yogesh Bhanushali <dbhanushali@vmware.com>
Dieter Reuter <dieter.reuter@me.com> Dieter Reuter <dieter.reuter@me.com>
Dilep Dev <34891655+DilepDev@users.noreply.github.com>
Dima Stopel <dima@twistlock.com> Dima Stopel <dima@twistlock.com>
Dimitry Andric <d.andric@activevideo.com> Dimitry Andric <d.andric@activevideo.com>
Ding Fei <dingfei@stars.org.cn> Ding Fei <dingfei@stars.org.cn>
@ -314,8 +308,6 @@ George MacRorie <gmacr31@gmail.com>
George Margaritis <gmargaritis@protonmail.com> George Margaritis <gmargaritis@protonmail.com>
George Xie <georgexsh@gmail.com> George Xie <georgexsh@gmail.com>
Gianluca Borello <g.borello@gmail.com> Gianluca Borello <g.borello@gmail.com>
Giau. Tran Minh <hello@giautm.dev>
Giedrius Jonikas <giedriusj1@gmail.com>
Gildas Cuisinier <gildas.cuisinier@gcuisinier.net> Gildas Cuisinier <gildas.cuisinier@gcuisinier.net>
Gio d'Amelio <giodamelio@gmail.com> Gio d'Amelio <giodamelio@gmail.com>
Gleb Stsenov <gleb.stsenov@gmail.com> Gleb Stsenov <gleb.stsenov@gmail.com>
@ -352,7 +344,6 @@ Hugo Gabriel Eyherabide <hugogabriel.eyherabide@gmail.com>
huqun <huqun@zju.edu.cn> huqun <huqun@zju.edu.cn>
Huu Nguyen <huu@prismskylabs.com> Huu Nguyen <huu@prismskylabs.com>
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com> Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com>
Iain MacDonald <IJMacD@gmail.com>
Iain Samuel McLean Elder <iain@isme.es> Iain Samuel McLean Elder <iain@isme.es>
Ian Campbell <ian.campbell@docker.com> Ian Campbell <ian.campbell@docker.com>
Ian Philpot <ian.philpot@microsoft.com> Ian Philpot <ian.philpot@microsoft.com>
@ -402,7 +393,6 @@ Jesse Adametz <jesseadametz@gmail.com>
Jessica Frazelle <jess@oxide.computer> Jessica Frazelle <jess@oxide.computer>
Jezeniel Zapanta <jpzapanta22@gmail.com> Jezeniel Zapanta <jpzapanta22@gmail.com>
Jian Zhang <zhangjian.fnst@cn.fujitsu.com> Jian Zhang <zhangjian.fnst@cn.fujitsu.com>
Jianyong Wu <wujianyong@hygon.cn>
Jie Luo <luo612@zju.edu.cn> Jie Luo <luo612@zju.edu.cn>
Jilles Oldenbeuving <ojilles@gmail.com> Jilles Oldenbeuving <ojilles@gmail.com>
Jim Chen <njucjc@gmail.com> Jim Chen <njucjc@gmail.com>
@ -456,7 +446,6 @@ Julian <gitea+julian@ic.thejulian.uk>
Julien Barbier <write0@gmail.com> Julien Barbier <write0@gmail.com>
Julien Kassar <github@kassisol.com> Julien Kassar <github@kassisol.com>
Julien Maitrehenry <julien.maitrehenry@me.com> Julien Maitrehenry <julien.maitrehenry@me.com>
Julio Cesar Garcia <juliogarciamelgarejo@gmail.com>
Justas Brazauskas <brazauskasjustas@gmail.com> Justas Brazauskas <brazauskasjustas@gmail.com>
Justin Chadwell <me@jedevc.com> Justin Chadwell <me@jedevc.com>
Justin Cormack <justin.cormack@docker.com> Justin Cormack <justin.cormack@docker.com>
@ -501,22 +490,19 @@ Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp>
Kyle Mitofsky <Kylemit@gmail.com> Kyle Mitofsky <Kylemit@gmail.com>
Lachlan Cooper <lachlancooper@gmail.com> Lachlan Cooper <lachlancooper@gmail.com>
Lai Jiangshan <jiangshanlai@gmail.com> Lai Jiangshan <jiangshanlai@gmail.com>
Lajos Papp <lajos.papp@sequenceiq.com>
Lars Kellogg-Stedman <lars@redhat.com> Lars Kellogg-Stedman <lars@redhat.com>
Laura Brehm <laurabrehm@hey.com> Laura Brehm <laurabrehm@hey.com>
Laura Frank <ljfrank@gmail.com> Laura Frank <ljfrank@gmail.com>
Laurent Erignoux <lerignoux@gmail.com> Laurent Erignoux <lerignoux@gmail.com>
Laurent Goderre <laurent.goderre@docker.com>
Lee Gaines <eightlimbed@gmail.com> Lee Gaines <eightlimbed@gmail.com>
Lei Jitang <leijitang@huawei.com> Lei Jitang <leijitang@huawei.com>
Lennie <github@consolejunkie.net> Lennie <github@consolejunkie.net>
lentil32 <lentil32@icloud.com>
Leo Gallucci <elgalu3@gmail.com> Leo Gallucci <elgalu3@gmail.com>
Leonid Skorospelov <leosko94@gmail.com> Leonid Skorospelov <leosko94@gmail.com>
Lewis Daly <lewisdaly@me.com> Lewis Daly <lewisdaly@me.com>
Li Fu Bang <lifubang@acmcoder.com> Li Fu Bang <lifubang@acmcoder.com>
Li Yi <denverdino@gmail.com> Li Yi <denverdino@gmail.com>
Li Zeghong <zeghong@hotmail.com> Li Yi <weiyuan.yl@alibaba-inc.com>
Liang-Chi Hsieh <viirya@gmail.com> Liang-Chi Hsieh <viirya@gmail.com>
Lihua Tang <lhtang@alauda.io> Lihua Tang <lhtang@alauda.io>
Lily Guo <lily.guo@docker.com> Lily Guo <lily.guo@docker.com>
@ -529,7 +515,6 @@ lixiaobing10051267 <li.xiaobing1@zte.com.cn>
Lloyd Dewolf <foolswisdom@gmail.com> Lloyd Dewolf <foolswisdom@gmail.com>
Lorenzo Fontana <lo@linux.com> Lorenzo Fontana <lo@linux.com>
Louis Opter <kalessin@kalessin.fr> Louis Opter <kalessin@kalessin.fr>
Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
Luca Favatella <luca.favatella@erlang-solutions.com> Luca Favatella <luca.favatella@erlang-solutions.com>
Luca Marturana <lucamarturana@gmail.com> Luca Marturana <lucamarturana@gmail.com>
Lucas Chan <lucas-github@lucaschan.com> Lucas Chan <lucas-github@lucaschan.com>
@ -574,7 +559,6 @@ Matt Robenolt <matt@ydekproductions.com>
Matteo Orefice <matteo.orefice@bites4bits.software> Matteo Orefice <matteo.orefice@bites4bits.software>
Matthew Heon <mheon@redhat.com> Matthew Heon <mheon@redhat.com>
Matthieu Hauglustaine <matt.hauglustaine@gmail.com> Matthieu Hauglustaine <matt.hauglustaine@gmail.com>
Matthieu MOREL <matthieu.morel35@gmail.com>
Mauro Porras P <mauroporrasp@gmail.com> Mauro Porras P <mauroporrasp@gmail.com>
Max Shytikov <mshytikov@gmail.com> Max Shytikov <mshytikov@gmail.com>
Max-Julian Pogner <max-julian@pogner.at> Max-Julian Pogner <max-julian@pogner.at>
@ -582,7 +566,6 @@ Maxime Petazzoni <max@signalfuse.com>
Maximillian Fan Xavier <maximillianfx@gmail.com> Maximillian Fan Xavier <maximillianfx@gmail.com>
Mei ChunTao <mei.chuntao@zte.com.cn> Mei ChunTao <mei.chuntao@zte.com.cn>
Melroy van den Berg <melroy@melroy.org> Melroy van den Berg <melroy@melroy.org>
Mert Şişmanoğlu <mert190737fb@gmail.com>
Metal <2466052+tedhexaflow@users.noreply.github.com> Metal <2466052+tedhexaflow@users.noreply.github.com>
Micah Zoltu <micah@newrelic.com> Micah Zoltu <micah@newrelic.com>
Michael A. Smith <michael@smith-li.com> Michael A. Smith <michael@smith-li.com>
@ -615,9 +598,7 @@ Mindaugas Rukas <momomg@gmail.com>
Miroslav Gula <miroslav.gula@naytrolabs.com> Miroslav Gula <miroslav.gula@naytrolabs.com>
Misty Stanley-Jones <misty@docker.com> Misty Stanley-Jones <misty@docker.com>
Mohammad Banikazemi <mb@us.ibm.com> Mohammad Banikazemi <mb@us.ibm.com>
Mohammad Hossein <mhm98035@gmail.com>
Mohammed Aaqib Ansari <maaquib@gmail.com> Mohammed Aaqib Ansari <maaquib@gmail.com>
Mohammed Aminu Futa <mohammedfuta2000@gmail.com>
Mohini Anne Dsouza <mohini3917@gmail.com> Mohini Anne Dsouza <mohini3917@gmail.com>
Moorthy RS <rsmoorthy@gmail.com> Moorthy RS <rsmoorthy@gmail.com>
Morgan Bauer <mbauer@us.ibm.com> Morgan Bauer <mbauer@us.ibm.com>
@ -652,11 +633,9 @@ Nicolas De Loof <nicolas.deloof@gmail.com>
Nikhil Chawla <chawlanikhil24@gmail.com> Nikhil Chawla <chawlanikhil24@gmail.com>
Nikolas Garofil <nikolas.garofil@uantwerpen.be> Nikolas Garofil <nikolas.garofil@uantwerpen.be>
Nikolay Milovanov <nmil@itransformers.net> Nikolay Milovanov <nmil@itransformers.net>
NinaLua <iturf@sina.cn>
Nir Soffer <nsoffer@redhat.com> Nir Soffer <nsoffer@redhat.com>
Nishant Totla <nishanttotla@gmail.com> Nishant Totla <nishanttotla@gmail.com>
NIWA Hideyuki <niwa.niwa@nifty.ne.jp> NIWA Hideyuki <niwa.niwa@nifty.ne.jp>
Noah Silas <noah@hustle.com>
Noah Treuhaft <noah.treuhaft@docker.com> Noah Treuhaft <noah.treuhaft@docker.com>
O.S. Tezer <ostezer@gmail.com> O.S. Tezer <ostezer@gmail.com>
Oded Arbel <oded@geek.co.il> Oded Arbel <oded@geek.co.il>
@ -674,12 +653,10 @@ Patrick Böänziger <patrick.baenziger@bsi-software.com>
Patrick Daigle <114765035+pdaig@users.noreply.github.com> Patrick Daigle <114765035+pdaig@users.noreply.github.com>
Patrick Hemmer <patrick.hemmer@gmail.com> Patrick Hemmer <patrick.hemmer@gmail.com>
Patrick Lang <plang@microsoft.com> Patrick Lang <plang@microsoft.com>
Patrick St. laurent <patrick@saint-laurent.us>
Paul <paul9869@gmail.com> Paul <paul9869@gmail.com>
Paul Kehrer <paul.l.kehrer@gmail.com> Paul Kehrer <paul.l.kehrer@gmail.com>
Paul Lietar <paul@lietar.net> Paul Lietar <paul@lietar.net>
Paul Mulders <justinkb@gmail.com> Paul Mulders <justinkb@gmail.com>
Paul Rogalski <mail@paul-rogalski.de>
Paul Seyfert <pseyfert.mathphys@gmail.com> Paul Seyfert <pseyfert.mathphys@gmail.com>
Paul Weaver <pauweave@cisco.com> Paul Weaver <pauweave@cisco.com>
Pavel Pospisil <pospispa@gmail.com> Pavel Pospisil <pospispa@gmail.com>
@ -701,6 +678,7 @@ Philip Alexander Etling <paetling@gmail.com>
Philipp Gillé <philipp.gille@gmail.com> Philipp Gillé <philipp.gille@gmail.com>
Philipp Schmied <pschmied@schutzwerk.com> Philipp Schmied <pschmied@schutzwerk.com>
Phong Tran <tran.pho@northeastern.edu> Phong Tran <tran.pho@northeastern.edu>
pidster <pid@pidster.com>
Pieter E Smit <diepes@github.com> Pieter E Smit <diepes@github.com>
pixelistik <pixelistik@users.noreply.github.com> pixelistik <pixelistik@users.noreply.github.com>
Pratik Karki <prertik@outlook.com> Pratik Karki <prertik@outlook.com>
@ -760,7 +738,6 @@ Samuel Cochran <sj26@sj26.com>
Samuel Karp <skarp@amazon.com> Samuel Karp <skarp@amazon.com>
Sandro Jäckel <sandro.jaeckel@gmail.com> Sandro Jäckel <sandro.jaeckel@gmail.com>
Santhosh Manohar <santhosh@docker.com> Santhosh Manohar <santhosh@docker.com>
Sarah Sanders <sarah.sanders@docker.com>
Sargun Dhillon <sargun@netflix.com> Sargun Dhillon <sargun@netflix.com>
Saswat Bhattacharya <sas.saswat@gmail.com> Saswat Bhattacharya <sas.saswat@gmail.com>
Saurabh Kumar <saurabhkumar0184@gmail.com> Saurabh Kumar <saurabhkumar0184@gmail.com>
@ -793,7 +770,6 @@ Spencer Brown <spencer@spencerbrown.org>
Spring Lee <xi.shuai@outlook.com> Spring Lee <xi.shuai@outlook.com>
squeegels <lmscrewy@gmail.com> squeegels <lmscrewy@gmail.com>
Srini Brahmaroutu <srbrahma@us.ibm.com> Srini Brahmaroutu <srbrahma@us.ibm.com>
Stavros Panakakis <stavrospanakakis@gmail.com>
Stefan S. <tronicum@user.github.com> Stefan S. <tronicum@user.github.com>
Stefan Scherer <stefan.scherer@docker.com> Stefan Scherer <stefan.scherer@docker.com>
Stefan Weil <sw@weilnetz.de> Stefan Weil <sw@weilnetz.de>
@ -804,7 +780,6 @@ Steve Durrheimer <s.durrheimer@gmail.com>
Steve Richards <steve.richards@docker.com> Steve Richards <steve.richards@docker.com>
Steven Burgess <steven.a.burgess@hotmail.com> Steven Burgess <steven.a.burgess@hotmail.com>
Stoica-Marcu Floris-Andrei <floris.sm@gmail.com> Stoica-Marcu Floris-Andrei <floris.sm@gmail.com>
Stuart Williams <pid@pidster.com>
Subhajit Ghosh <isubuz.g@gmail.com> Subhajit Ghosh <isubuz.g@gmail.com>
Sun Jianbo <wonderflow.sun@gmail.com> Sun Jianbo <wonderflow.sun@gmail.com>
Sune Keller <absukl@almbrand.dk> Sune Keller <absukl@almbrand.dk>
@ -892,7 +867,6 @@ Wang Yumu <37442693@qq.com>
Wataru Ishida <ishida.wataru@lab.ntt.co.jp> Wataru Ishida <ishida.wataru@lab.ntt.co.jp>
Wayne Song <wsong@docker.com> Wayne Song <wsong@docker.com>
Wen Cheng Ma <wenchma@cn.ibm.com> Wen Cheng Ma <wenchma@cn.ibm.com>
Wenlong Zhang <zhangwenlong@loongson.cn>
Wenzhi Liang <wenzhi.liang@gmail.com> Wenzhi Liang <wenzhi.liang@gmail.com>
Wes Morgan <cap10morgan@gmail.com> Wes Morgan <cap10morgan@gmail.com>
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn> Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
@ -934,4 +908,3 @@ Zhuo Zhi <h.dwwwwww@gmail.com>
Átila Camurça Alves <camurca.home@gmail.com> Átila Camurça Alves <camurca.home@gmail.com>
Александр Менщиков <__Singleton__@hackerdom.ru> Александр Менщиков <__Singleton__@hackerdom.ru>
徐俊杰 <paco.xu@daocloud.io> 徐俊杰 <paco.xu@daocloud.io>
林博仁 Buo-ren Lin <Buo.Ren.Lin@gmail.com>

View File

@ -66,7 +66,7 @@ anybody starts working on it.
We are always thrilled to receive pull requests. We do our best to process them We are always thrilled to receive pull requests. We do our best to process them
quickly. If your pull request is not accepted on the first try, quickly. If your pull request is not accepted on the first try,
don't get discouraged! Our contributor's guide explains [the review process we don't get discouraged! Our contributor's guide explains [the review process we
use for simple changes](https://github.com/docker/docker/blob/master/project/REVIEWING.md). use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contribution/).
### Talking to other Docker users and contributors ### Talking to other Docker users and contributors
@ -124,8 +124,8 @@ submitting a pull request.
Update the documentation when creating or modifying features. Test your Update the documentation when creating or modifying features. Test your
documentation changes for clarity, concision, and correctness, as well as a documentation changes for clarity, concision, and correctness, as well as a
clean documentation build. See our contributors guide for [our style clean documentation build. See our contributors guide for [our style
guide](https://docs.docker.com/contribute/style/grammar/) and instructions on [building guide](https://docs.docker.com/opensource/doc-style) and instructions on [building
the documentation](https://docs.docker.com/contribute/). the documentation](https://docs.docker.com/opensource/project/test-and-docs/#build-and-test-the-documentation).
Write clean code. Universally formatted code promotes ease of writing, reading, Write clean code. Universally formatted code promotes ease of writing, reading,
and maintenance. Always run `gofmt -s -w file.go` on each changed file before and maintenance. Always run `gofmt -s -w file.go` on each changed file before
@ -134,41 +134,9 @@ committing your changes. Most editors have plug-ins that do this automatically.
Pull request descriptions should be as clear as possible and include a reference Pull request descriptions should be as clear as possible and include a reference
to all the issues that they address. to all the issues that they address.
Commit messages must be written in the imperative mood (max. 72 chars), followed Commit messages must start with a capitalized and short summary (max. 50 chars)
by an optional, more detailed explanatory text usually expanding on written in the imperative, followed by an optional, more detailed explanatory
why the work is necessary. The explanatory text should be separated by an text which is separated from the summary by an empty line.
empty line.
The commit message *could* have a prefix scoping the change, however this is
not enforced. Common prefixes are `docs: <message>`, `vendor: <message>`,
`chore: <message>` or the package/area related to the change such as `pkg/foo: <message>`
or `telemetry: <message>`.
A standard commit.
```
Fix the exploding flux capacitor
A call to function A causes the flux capacitor to blow up every time
the sun and the moon align.
```
Using a package as prefix.
```
pkg/foo: prevent panic in flux capacitor
Calling function A causes the flux capacitor to blow up every time
the sun and the moon align.
```
Updating a specific vendored package.
```
vendor: github.com/docker/docker 6ac445c42bad (master, v28.0-dev)
```
Fixing a broken docs link.
```
docs: fix style/lint issues in deprecated.md
```
Code review comments may be added to your pull request. Discuss, then make the Code review comments may be added to your pull request. Discuss, then make the
suggested modifications and push additional commits to your feature branch. Post suggested modifications and push additional commits to your feature branch. Post

View File

@ -1,37 +1,22 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG BASE_VARIANT=alpine ARG BASE_VARIANT=alpine
ARG ALPINE_VERSION=3.20
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
# that's also available as alpine image variant for the Golang version used.
ARG ALPINE_VERSION=3.22
ARG BASE_DEBIAN_DISTRO=bookworm ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.24.5 ARG GO_VERSION=1.22.7
ARG XX_VERSION=1.6.1 ARG XX_VERSION=1.5.0
ARG GOVERSIONINFO_VERSION=v1.4.1 ARG GOVERSIONINFO_VERSION=v1.3.0
ARG GOTESTSUM_VERSION=v1.10.0
# GOTESTSUM_VERSION sets the version of gotestsum to install in the dev container. ARG BUILDX_VERSION=0.16.1
# It must be a valid tag in the https://github.com/gotestyourself/gotestsum repository. ARG COMPOSE_VERSION=v2.29.0
ARG GOTESTSUM_VERSION=v1.12.3
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
# It must be a tag in the docker.io/docker/buildx-bin image repository
# on Docker Hub.
ARG BUILDX_VERSION=0.25.0
# COMPOSE_VERSION is the version of compose to install in the dev container.
# It must be a tag in the docker.io/docker/compose-bin image repository
# on Docker Hub.
ARG COMPOSE_VERSION=v2.38.2
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-base-alpine FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-base-alpine
ENV GOTOOLCHAIN=local ENV GOTOOLCHAIN=local
COPY --link --from=xx / / COPY --link --from=xx / /
RUN apk add --no-cache bash clang lld llvm file git git-daemon RUN apk add --no-cache bash clang lld llvm file git
WORKDIR /go/src/github.com/docker/cli WORKDIR /go/src/github.com/docker/cli
FROM build-base-alpine AS build-alpine FROM build-base-alpine AS build-alpine
@ -78,7 +63,7 @@ ARG PACKAGER_NAME
COPY --link --from=goversioninfo /out/goversioninfo /usr/bin/goversioninfo COPY --link --from=goversioninfo /out/goversioninfo /usr/bin/goversioninfo
RUN --mount=type=bind,target=.,ro \ RUN --mount=type=bind,target=.,ro \
--mount=type=cache,target=/root/.cache \ --mount=type=cache,target=/root/.cache \
--mount=type=tmpfs,target=cmd/docker/winresources \ --mount=type=tmpfs,target=cli/winresources \
# override the default behavior of go with xx-go # override the default behavior of go with xx-go
xx-go --wrap && \ xx-go --wrap && \
# export GOCACHE=$(go env GOCACHE)/$(xx-info)$([ -f /etc/alpine-release ] && echo "alpine") && \ # export GOCACHE=$(go env GOCACHE)/$(xx-info)$([ -f /etc/alpine-release ] && echo "alpine") && \
@ -128,7 +113,7 @@ COPY --link --from=compose /docker-compose /usr/libexec/docker/cli-plugins/docke
COPY --link . . COPY --link . .
ENV DOCKER_BUILDKIT=1 ENV DOCKER_BUILDKIT=1
ENV PATH=/go/src/github.com/docker/cli/build:$PATH ENV PATH=/go/src/github.com/docker/cli/build:$PATH
CMD ["./scripts/test/e2e/entry"] CMD ./scripts/test/e2e/entry
FROM build-base-${BASE_VARIANT} AS dev FROM build-base-${BASE_VARIANT} AS dev
COPY --link . . COPY --link . .

View File

@ -52,7 +52,7 @@ shellcheck: ## run shellcheck validation
.PHONY: fmt .PHONY: fmt
fmt: ## run gofumpt (if present) or gofmt fmt: ## run gofumpt (if present) or gofmt
@if command -v gofumpt > /dev/null; then \ @if command -v gofumpt > /dev/null; then \
gofumpt -w -d -lang=1.23 . ; \ gofumpt -w -d -lang=1.21 . ; \
else \ else \
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \ go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \
fi fi
@ -67,63 +67,46 @@ dynbinary: ## build dynamically linked binary
.PHONY: plugins .PHONY: plugins
plugins: ## build example CLI plugins plugins: ## build example CLI plugins
scripts/build/plugins ./scripts/build/plugins
.PHONY: vendor .PHONY: vendor
vendor: ## update vendor with go modules vendor: ## update vendor with go modules
rm -rf vendor rm -rf vendor
scripts/with-go-mod.sh scripts/vendor update ./scripts/vendor update
.PHONY: validate-vendor .PHONY: validate-vendor
validate-vendor: ## validate vendor validate-vendor: ## validate vendor
scripts/with-go-mod.sh scripts/vendor validate ./scripts/vendor validate
.PHONY: mod-outdated .PHONY: mod-outdated
mod-outdated: ## check outdated dependencies mod-outdated: ## check outdated dependencies
scripts/with-go-mod.sh scripts/vendor outdated ./scripts/vendor outdated
.PHONY: authors .PHONY: authors
authors: ## generate AUTHORS file from git history authors: ## generate AUTHORS file from git history
scripts/docs/generate-authors.sh scripts/docs/generate-authors.sh
.PHONY: completion .PHONY: completion
completion: shell-completion completion: binary
completion: ## generate and install the shell-completion scripts completion: /etc/bash_completion.d/docker
# Note: this uses system-wide paths, and so may overwrite completion completion: ## generate and install the completion scripts
# scripts installed as part of deb/rpm packages.
#
# Given that this target is intended to debug/test updated versions, we could
# consider installing in per-user (~/.config, XDG_DATA_DIR) paths instead, but
# this will add more complexity.
#
# See https://github.com/docker/cli/pull/5770#discussion_r1927772710
install -D -p -m 0644 ./build/completion/bash/docker /usr/share/bash-completion/completions/docker
install -D -p -m 0644 ./build/completion/fish/docker.fish debian/docker-ce-cli/usr/share/fish/vendor_completions.d/docker.fish
install -D -p -m 0644 ./build/completion/zsh/_docker debian/docker-ce-cli/usr/share/zsh/vendor-completions/_docker
.PHONY: /etc/bash_completion.d/docker
build/docker: /etc/bash_completion.d/docker: ## generate and install the bash-completion script
# This target is used by the "shell-completion" target, which requires either mkdir -p /etc/bash_completion.d
# "binary" or "dynbinary" to have been built. We don't want to trigger those docker completion bash > /etc/bash_completion.d/docker
# to prevent replacing a static binary with a dynamic one, or vice-versa.
@echo "Run 'make binary' or 'make dynbinary' first" && exit 1
.PHONY: shell-completion
shell-completion: build/docker # requires either "binary" or "dynbinary" to be built.
shell-completion: ## generate shell-completion scripts
@ ./scripts/build/shell-completion
.PHONY: manpages .PHONY: manpages
manpages: ## generate man pages from go source and markdown manpages: ## generate man pages from go source and markdown
scripts/with-go-mod.sh scripts/docs/generate-man.sh scripts/docs/generate-man.sh
.PHONY: mddocs .PHONY: mddocs
mddocs: ## generate markdown files from go source mddocs: ## generate markdown files from go source
scripts/with-go-mod.sh scripts/docs/generate-md.sh scripts/docs/generate-md.sh
.PHONY: yamldocs .PHONY: yamldocs
yamldocs: ## generate documentation YAML files consumed by docs repo yamldocs: ## generate documentation YAML files consumed by docs repo
scripts/with-go-mod.sh scripts/docs/generate-yaml.sh scripts/docs/generate-yaml.sh
.PHONY: help .PHONY: help
help: ## print this help help: ## print this help

View File

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

View File

@ -1,44 +0,0 @@
# Security Policy
The maintainers of the Docker CLI take security seriously. If you discover
a security issue, please bring it to their attention right away!
## Reporting a Vulnerability
Please **DO NOT** file a public issue, instead send your report privately
to [security@docker.com](mailto:security@docker.com).
Reporter(s) can expect a response within 72 hours, acknowledging the issue was
received.
## Review Process
After receiving the report, an initial triage and technical analysis is
performed to confirm the report and determine its scope. We may request
additional information in this stage of the process.
Once a reviewer has confirmed the relevance of the report, a draft security
advisory will be created on GitHub. The draft advisory will be used to discuss
the issue with maintainers, the reporter(s), and where applicable, other
affected parties under embargo.
If the vulnerability is accepted, a timeline for developing a patch, public
disclosure, and patch release will be determined. If there is an embargo period
on public disclosure before the patch release, the reporter(s) are expected to
participate in the discussion of the timeline and abide by agreed upon dates
for public disclosure.
## Accreditation
Security reports are greatly appreciated and we will publicly thank you,
although we will keep your name confidential if you request it. We also like to
send gifts - if you're into swag, make sure to let us know. We do not currently
offer a paid security bounty program at this time.
## Supported Versions
This project uses long-lived branches to maintain releases, and follows
the maintenance cycle of the Moby project.
Refer to [BRANCHES-AND-TAGS.md](https://github.com/moby/moby/blob/master/project/BRANCHES-AND-TAGS.md)
in the default branch of the moby repository to learn about the current
maintenance status of each branch.

View File

@ -1 +1 @@
28.3.0-dev 27.0.1-dev

View File

@ -5,31 +5,31 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func main() { func main() {
plugin.Run(func(dockerCLI command.Cli) *cobra.Command { plugin.Run(func(dockerCli command.Cli) *cobra.Command {
goodbye := &cobra.Command{ goodbye := &cobra.Command{
Use: "goodbye", Use: "goodbye",
Short: "Say Goodbye instead of Hello", Short: "Say Goodbye instead of Hello",
Run: func(cmd *cobra.Command, _ []string) { Run: func(cmd *cobra.Command, _ []string) {
_, _ = fmt.Fprintln(dockerCLI.Out(), "Goodbye World!") fmt.Fprintln(dockerCli.Out(), "Goodbye World!")
}, },
} }
apiversion := &cobra.Command{ apiversion := &cobra.Command{
Use: "apiversion", Use: "apiversion",
Short: "Print the API version of the server", Short: "Print the API version of the server",
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
apiClient := dockerCLI.Client() cli := dockerCli.Client()
ping, err := apiClient.Ping(context.Background()) ping, err := cli.Ping(context.Background())
if err != nil { if err != nil {
return err return err
} }
_, _ = fmt.Println(ping.APIVersion) fmt.Println(ping.APIVersion)
return nil return nil
}, },
} }
@ -38,7 +38,7 @@ func main() {
Use: "exitstatus2", Use: "exitstatus2",
Short: "Exit with status 2", Short: "Exit with status 2",
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Exiting with error status 2") fmt.Fprintln(dockerCli.Err(), "Exiting with error status 2")
os.Exit(2) os.Exit(2)
return nil return nil
}, },
@ -56,33 +56,33 @@ func main() {
return err return err
} }
if preRun { if preRun {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Plugin PersistentPreRunE called") fmt.Fprintf(dockerCli.Err(), "Plugin PersistentPreRunE called")
} }
return nil return nil
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if debug { if debug {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Plugin debug mode enabled") fmt.Fprintf(dockerCli.Err(), "Plugin debug mode enabled")
} }
switch optContext { switch optContext {
case "Christmas": case "Christmas":
_, _ = fmt.Fprintln(dockerCLI.Out(), "Merry Christmas!") fmt.Fprintf(dockerCli.Out(), "Merry Christmas!\n")
return nil return nil
case "": case "":
// nothing // nothing
} }
if who == "" { if who == "" {
who, _ = dockerCLI.ConfigFile().PluginConfig("helloworld", "who") who, _ = dockerCli.ConfigFile().PluginConfig("helloworld", "who")
} }
if who == "" { if who == "" {
who = "World" who = "World"
} }
_, _ = fmt.Fprintln(dockerCLI.Out(), "Hello", who) fmt.Fprintf(dockerCli.Out(), "Hello %s!\n", who)
dockerCLI.ConfigFile().SetPluginConfig("helloworld", "lastwho", who) dockerCli.ConfigFile().SetPluginConfig("helloworld", "lastwho", who)
return dockerCLI.ConfigFile().Save() return dockerCli.ConfigFile().Save()
}, },
} }
@ -97,7 +97,7 @@ func main() {
cmd.AddCommand(goodbye, apiversion, exitStatus2) cmd.AddCommand(goodbye, apiversion, exitStatus2)
return cmd return cmd
}, },
metadata.Metadata{ manager.Metadata{
SchemaVersion: "0.1.0", SchemaVersion: "0.1.0",
Vendor: "Docker Inc.", Vendor: "Docker Inc.",
Version: "testing", Version: "testing",

View File

@ -11,8 +11,8 @@ func PrintNextSteps(out io.Writer, messages []string) {
if len(messages) == 0 { if len(messages) == 0 {
return return
} }
_, _ = fmt.Fprintln(out, aec.Bold.Apply("\nWhat's next:")) fmt.Fprintln(out, aec.Bold.Apply("\nWhat's next:"))
for _, n := range messages { for _, n := range messages {
_, _ = fmt.Fprintln(out, " ", n) _, _ = fmt.Fprintf(out, " %s\n", n)
} }
} }

View File

@ -1,30 +0,0 @@
package manager
import "github.com/docker/cli/cli-plugins/metadata"
const (
// CommandAnnotationPlugin is added to every stub command added by
// AddPluginCommandStubs with the value "true" and so can be
// used to distinguish plugin stubs from regular commands.
CommandAnnotationPlugin = metadata.CommandAnnotationPlugin
// CommandAnnotationPluginVendor is added to every stub command
// added by AddPluginCommandStubs and contains the vendor of
// that plugin.
CommandAnnotationPluginVendor = metadata.CommandAnnotationPluginVendor
// CommandAnnotationPluginVersion is added to every stub command
// added by AddPluginCommandStubs and contains the version of
// that plugin.
CommandAnnotationPluginVersion = metadata.CommandAnnotationPluginVersion
// CommandAnnotationPluginInvalid is added to any stub command
// added by AddPluginCommandStubs for an invalid command (that
// is, one which failed it's candidate test) and contains the
// reason for the failure.
CommandAnnotationPluginInvalid = metadata.CommandAnnotationPluginInvalid
// CommandAnnotationPluginCommandPath is added to overwrite the
// command path for a plugin invocation.
CommandAnnotationPluginCommandPath = metadata.CommandAnnotationPluginCommandPath
)

View File

@ -1,10 +1,6 @@
package manager package manager
import ( import "os/exec"
"os/exec"
"github.com/docker/cli/cli-plugins/metadata"
)
// Candidate represents a possible plugin candidate, for mocking purposes // Candidate represents a possible plugin candidate, for mocking purposes
type Candidate interface { type Candidate interface {
@ -21,5 +17,5 @@ func (c *candidate) Path() string {
} }
func (c *candidate) Metadata() ([]byte, error) { func (c *candidate) Metadata() ([]byte, error) {
return exec.Command(c.path, metadata.MetadataSubcommandName).Output() // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments" return exec.Command(c.path, MetadataSubcommandName).Output()
} }

View File

@ -6,10 +6,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/assert/cmp"
) )
type fakeCandidate struct { type fakeCandidate struct {
@ -31,10 +30,10 @@ func (c *fakeCandidate) Metadata() ([]byte, error) {
func TestValidateCandidate(t *testing.T) { func TestValidateCandidate(t *testing.T) {
const ( const (
goodPluginName = metadata.NamePrefix + "goodplugin" goodPluginName = NamePrefix + "goodplugin"
builtinName = metadata.NamePrefix + "builtin" builtinName = NamePrefix + "builtin"
builtinAlias = metadata.NamePrefix + "alias" builtinAlias = NamePrefix + "alias"
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble" badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456" badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
@ -44,9 +43,9 @@ func TestValidateCandidate(t *testing.T) {
fakeroot := &cobra.Command{Use: "docker"} fakeroot := &cobra.Command{Use: "docker"}
fakeroot.AddCommand(&cobra.Command{ fakeroot.AddCommand(&cobra.Command{
Use: strings.TrimPrefix(builtinName, metadata.NamePrefix), Use: strings.TrimPrefix(builtinName, NamePrefix),
Aliases: []string{ Aliases: []string{
strings.TrimPrefix(builtinAlias, metadata.NamePrefix), strings.TrimPrefix(builtinAlias, NamePrefix),
}, },
}) })
@ -60,7 +59,7 @@ func TestValidateCandidate(t *testing.T) {
}{ }{
/* Each failing one of the tests */ /* Each failing one of the tests */
{name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"}, {name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
{name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix)}, {name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", NamePrefix)},
{name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"}, {name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
{name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`}, {name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`},
{name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`}, {name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`},
@ -81,11 +80,11 @@ func TestValidateCandidate(t *testing.T) {
assert.ErrorContains(t, err, tc.err) assert.ErrorContains(t, err, tc.err)
case tc.invalid != "": case tc.invalid != "":
assert.NilError(t, err) assert.NilError(t, err)
assert.Assert(t, is.ErrorType(p.Err, reflect.TypeOf(&pluginError{}))) assert.Assert(t, cmp.ErrorType(p.Err, reflect.TypeOf(&pluginError{})))
assert.ErrorContains(t, p.Err, tc.invalid) assert.ErrorContains(t, p.Err, tc.invalid)
default: default:
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, metadata.NamePrefix+p.Name, goodPluginName) assert.Equal(t, NamePrefix+p.Name, goodPluginName)
assert.Equal(t, p.SchemaVersion, "0.1.0") assert.Equal(t, p.SchemaVersion, "0.1.0")
assert.Equal(t, p.Vendor, "e2e-testing") assert.Equal(t, p.Vendor, "e2e-testing")
} }

View File

@ -2,12 +2,41 @@ package manager
import ( import (
"fmt" "fmt"
"net/url"
"os" "os"
"strings"
"sync" "sync"
"github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.opentelemetry.io/otel/attribute"
)
const (
// CommandAnnotationPlugin is added to every stub command added by
// AddPluginCommandStubs with the value "true" and so can be
// used to distinguish plugin stubs from regular commands.
CommandAnnotationPlugin = "com.docker.cli.plugin"
// CommandAnnotationPluginVendor is added to every stub command
// added by AddPluginCommandStubs and contains the vendor of
// that plugin.
CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor"
// CommandAnnotationPluginVersion is added to every stub command
// added by AddPluginCommandStubs and contains the version of
// that plugin.
CommandAnnotationPluginVersion = "com.docker.cli.plugin.version"
// CommandAnnotationPluginInvalid is added to any stub command
// added by AddPluginCommandStubs for an invalid command (that
// is, one which failed it's candidate test) and contains the
// reason for the failure.
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
// CommandAnnotationPluginCommandPath is added to overwrite the
// command path for a plugin invocation.
CommandAnnotationPluginCommandPath = "com.docker.cli.plugin.command_path"
) )
var pluginCommandStubsOnce sync.Once var pluginCommandStubsOnce sync.Once
@ -15,25 +44,26 @@ var pluginCommandStubsOnce sync.Once
// AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid // AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid
// plugin. The command stubs will have several annotations added, see // plugin. The command stubs will have several annotations added, see
// `CommandAnnotationPlugin*`. // `CommandAnnotationPlugin*`.
func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (err error) { func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err error) {
pluginCommandStubsOnce.Do(func() { pluginCommandStubsOnce.Do(func() {
var plugins []Plugin var plugins []Plugin
plugins, err = ListPlugins(dockerCLI, rootCmd) plugins, err = ListPlugins(dockerCli, rootCmd)
if err != nil { if err != nil {
return return
} }
for _, p := range plugins { for _, p := range plugins {
p := p
vendor := p.Vendor vendor := p.Vendor
if vendor == "" { if vendor == "" {
vendor = "unknown" vendor = "unknown"
} }
annotations := map[string]string{ annotations := map[string]string{
metadata.CommandAnnotationPlugin: "true", CommandAnnotationPlugin: "true",
metadata.CommandAnnotationPluginVendor: vendor, CommandAnnotationPluginVendor: vendor,
metadata.CommandAnnotationPluginVersion: p.Version, CommandAnnotationPluginVersion: p.Version,
} }
if p.Err != nil { if p.Err != nil {
annotations[metadata.CommandAnnotationPluginInvalid] = p.Err.Error() annotations[CommandAnnotationPluginInvalid] = p.Err.Error()
} }
rootCmd.AddCommand(&cobra.Command{ rootCmd.AddCommand(&cobra.Command{
Use: p.Name, Use: p.Name,
@ -52,7 +82,7 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
cmd.HelpFunc()(rootCmd, args) cmd.HelpFunc()(rootCmd, args)
return nil return nil
} }
return fmt.Errorf("docker: unknown command: docker %s\n\nRun 'docker --help' for more information", cmd.Name()) return fmt.Errorf("docker: '%s' is not a docker command.\nSee 'docker --help'", cmd.Name())
}, },
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// Delegate completion to plugin // Delegate completion to plugin
@ -60,7 +90,7 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
cargs = append(cargs, args...) cargs = append(cargs, args...)
cargs = append(cargs, toComplete) cargs = append(cargs, toComplete)
os.Args = cargs os.Args = cargs
runCommand, runErr := PluginRunCommand(dockerCLI, p.Name, cmd) runCommand, runErr := PluginRunCommand(dockerCli, p.Name, cmd)
if runErr != nil { if runErr != nil {
return nil, cobra.ShellCompDirectiveError return nil, cobra.ShellCompDirectiveError
} }
@ -75,3 +105,44 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
}) })
return err return err
} }
const (
dockerCliAttributePrefix = attribute.Key("docker.cli")
cobraCommandPath = attribute.Key("cobra.command_path")
)
func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
commandPath := cmd.Annotations[CommandAnnotationPluginCommandPath]
if commandPath == "" {
commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name)
}
attrSet := attribute.NewSet(
cobraCommandPath.String(commandPath),
)
kvs := make([]attribute.KeyValue, 0, attrSet.Len())
for iter := attrSet.Iter(); iter.Next(); {
attr := iter.Attribute()
kvs = append(kvs, attribute.KeyValue{
Key: dockerCliAttributePrefix + "." + attr.Key,
Value: attr.Value,
})
}
return attribute.NewSet(kvs...)
}
func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string {
if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 {
// values in environment variables need to be in baggage format
// otel/baggage package can be used after update to v1.22, currently it encodes incorrectly
attrsSlice := make([]string, attrs.Len())
for iter := attrs.Iter(); iter.Next(); {
i, v := iter.IndexedAttribute()
attrsSlice[i] = string(v.Key) + "=" + url.PathEscape(v.Value.AsString())
}
env = append(env, ResourceAttributesEnvvar+"="+strings.Join(attrsSlice, ","))
}
return env
}

View File

@ -1,26 +0,0 @@
package manager
import (
"testing"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
)
func TestPluginResourceAttributesEnvvar(t *testing.T) {
cmd := &cobra.Command{
Annotations: map[string]string{
cobra.CommandDisplayNameAnnotation: "docker",
},
}
// Ensure basic usage is fine.
env := appendPluginResourceAttributesEnvvar(nil, cmd, Plugin{Name: "compose"})
assert.DeepEqual(t, []string{"OTEL_RESOURCE_ATTRIBUTES=docker.cli.cobra.command_path=docker%20compose"}, env)
// Add a user-based environment variable to OTEL_RESOURCE_ATTRIBUTES.
t.Setenv("OTEL_RESOURCE_ATTRIBUTES", "a.b.c=foo")
env = appendPluginResourceAttributesEnvvar(nil, cmd, Plugin{Name: "compose"})
assert.DeepEqual(t, []string{"OTEL_RESOURCE_ATTRIBUTES=a.b.c=foo,docker.cli.cobra.command_path=docker%20compose"}, env)
}

View File

@ -1,10 +1,10 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.23 //go:build go1.21
package manager package manager
import ( import (
"fmt" "github.com/pkg/errors"
) )
// pluginError is set as Plugin.Err by NewPlugin if the plugin // pluginError is set as Plugin.Err by NewPlugin if the plugin
@ -39,16 +39,16 @@ func (e *pluginError) MarshalText() (text []byte, err error) {
} }
// wrapAsPluginError wraps an error in a pluginError with an // wrapAsPluginError wraps an error in a pluginError with an
// additional message. // additional message, analogous to errors.Wrapf.
func wrapAsPluginError(err error, msg string) error { func wrapAsPluginError(err error, msg string) error {
if err == nil { if err == nil {
return nil return nil
} }
return &pluginError{cause: fmt.Errorf("%s: %w", msg, err)} return &pluginError{cause: errors.Wrap(err, msg)}
} }
// NewPluginError creates a new pluginError, analogous to // NewPluginError creates a new pluginError, analogous to
// errors.Errorf. // errors.Errorf.
func NewPluginError(msg string, args ...any) error { func NewPluginError(msg string, args ...any) error {
return &pluginError{cause: fmt.Errorf(msg, args...)} return &pluginError{cause: errors.Errorf(msg, args...)}
} }

View File

@ -6,8 +6,7 @@ import (
"strings" "strings"
"github.com/docker/cli/cli-plugins/hooks" "github.com/docker/cli/cli-plugins/hooks"
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/configfile"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -30,28 +29,29 @@ type HookPluginData struct {
// a main CLI command was executed. It calls the hook subcommand for all // a main CLI command was executed. It calls the hook subcommand for all
// present CLI plugins that declare support for hooks in their metadata and // present CLI plugins that declare support for hooks in their metadata and
// parses/prints their responses. // parses/prints their responses.
func RunCLICommandHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) { func RunCLICommandHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ") commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
flags := getCommandFlags(subCommand) flags := getCommandFlags(subCommand)
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, cmdErrorMessage) runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, cmdErrorMessage)
} }
// RunPluginHooks is the entrypoint for the hooks execution flow // RunPluginHooks is the entrypoint for the hooks execution flow
// after a plugin command was just executed by the CLI. // after a plugin command was just executed by the CLI.
func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, args []string) { func RunPluginHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) {
commandName := strings.Join(args, " ") commandName := strings.Join(args, " ")
flags := getNaiveFlags(args) flags := getNaiveFlags(args)
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, "") runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, "")
} }
func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) { func runHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
nextSteps := invokeAndCollectHooks(ctx, cfg, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage) nextSteps := invokeAndCollectHooks(ctx, dockerCli, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
hooks.PrintNextSteps(subCommand.ErrOrStderr(), nextSteps)
hooks.PrintNextSteps(dockerCli.Err(), nextSteps)
} }
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string { func invokeAndCollectHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
// check if the context was cancelled before invoking hooks // check if the context was cancelled before invoking hooks
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -59,20 +59,19 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
default: default:
} }
pluginsCfg := cfg.Plugins pluginsCfg := dockerCli.ConfigFile().Plugins
if pluginsCfg == nil { if pluginsCfg == nil {
return nil return nil
} }
pluginDirs := getPluginDirs(cfg)
nextSteps := make([]string, 0, len(pluginsCfg)) nextSteps := make([]string, 0, len(pluginsCfg))
for pluginName, pluginCfg := range pluginsCfg { for pluginName, cfg := range pluginsCfg {
match, ok := pluginMatch(pluginCfg, subCmdStr) match, ok := pluginMatch(cfg, subCmdStr)
if !ok { if !ok {
continue continue
} }
p, err := getPlugin(pluginName, pluginDirs, rootCmd) p, err := GetPlugin(pluginName, dockerCli, rootCmd)
if err != nil { if err != nil {
continue continue
} }

View File

@ -9,7 +9,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
@ -22,19 +22,17 @@ const (
// used to originally invoke the docker CLI when executing a // used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow // plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI. // the plugin to re-execute the original CLI.
ReexecEnvvar = metadata.ReexecEnvvar ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
// ResourceAttributesEnvvar is the name of the envvar that includes additional // ResourceAttributesEnvvar is the name of the envvar that includes additional
// resource attributes for OTEL. // resource attributes for OTEL.
//
// Deprecated: The "OTEL_RESOURCE_ATTRIBUTES" env-var is part of the OpenTelemetry specification; users should define their own const for this. This const will be removed in the next release.
ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES" ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES"
) )
// errPluginNotFound is the error returned when a plugin could not be found. // errPluginNotFound is the error returned when a plugin could not be found.
type errPluginNotFound string type errPluginNotFound string
func (errPluginNotFound) NotFound() {} func (e errPluginNotFound) NotFound() {}
func (e errPluginNotFound) Error() string { func (e errPluginNotFound) Error() string {
return "Error: No such CLI plugin: " + string(e) return "Error: No such CLI plugin: " + string(e)
@ -61,27 +59,29 @@ func IsNotFound(err error) bool {
// 3. Platform-specific defaultSystemPluginDirs. // 3. Platform-specific defaultSystemPluginDirs.
// //
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs // [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
func getPluginDirs(cfg *configfile.ConfigFile) []string { func getPluginDirs(cfg *configfile.ConfigFile) ([]string, error) {
var pluginDirs []string var pluginDirs []string
if cfg != nil { if cfg != nil {
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...) pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
} }
pluginDir := filepath.Join(config.Dir(), "cli-plugins") pluginDir, err := config.Path("cli-plugins")
pluginDirs = append(pluginDirs, pluginDir) if err != nil {
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...) return nil, err
return pluginDirs
} }
func addPluginCandidatesFromDir(res map[string][]string, d string) { pluginDirs = append(pluginDirs, pluginDir)
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
return pluginDirs, nil
}
func addPluginCandidatesFromDir(res map[string][]string, d string) error {
dentries, err := os.ReadDir(d) dentries, err := os.ReadDir(d)
// Silently ignore any directories which we cannot list (e.g. due to
// permissions or anything else) or which is not a directory
if err != nil { if err != nil {
return return err
} }
for _, dentry := range dentries { for _, dentry := range dentries {
switch dentry.Type() & os.ModeType { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list switch dentry.Type() & os.ModeType {
case 0, os.ModeSymlink: case 0, os.ModeSymlink:
// Regular file or symlink, keep going // Regular file or symlink, keep going
default: default:
@ -89,35 +89,52 @@ func addPluginCandidatesFromDir(res map[string][]string, d string) {
continue continue
} }
name := dentry.Name() name := dentry.Name()
if !strings.HasPrefix(name, metadata.NamePrefix) { if !strings.HasPrefix(name, NamePrefix) {
continue continue
} }
name = strings.TrimPrefix(name, metadata.NamePrefix) name = strings.TrimPrefix(name, NamePrefix)
var err error var err error
if name, err = trimExeSuffix(name); err != nil { if name, err = trimExeSuffix(name); err != nil {
continue continue
} }
res[name] = append(res[name], filepath.Join(d, dentry.Name())) res[name] = append(res[name], filepath.Join(d, dentry.Name()))
} }
return nil
} }
// listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority. // listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority.
func listPluginCandidates(dirs []string) map[string][]string { func listPluginCandidates(dirs []string) (map[string][]string, error) {
result := make(map[string][]string) result := make(map[string][]string)
for _, d := range dirs { for _, d := range dirs {
addPluginCandidatesFromDir(result, d) // Silently ignore any directories which we cannot
// Stat (e.g. due to permissions or anything else) or
// which is not a directory.
if fi, err := os.Stat(d); err != nil || !fi.IsDir() {
continue
} }
return result if err := addPluginCandidatesFromDir(result, d); err != nil {
// Silently ignore paths which don't exist.
if os.IsNotExist(err) {
continue
}
return nil, err // Or return partial result?
}
}
return result, nil
} }
// GetPlugin returns a plugin on the system by its name // GetPlugin returns a plugin on the system by its name
func GetPlugin(name string, dockerCLI config.Provider, rootcmd *cobra.Command) (*Plugin, error) { func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plugin, error) {
pluginDirs := getPluginDirs(dockerCLI.ConfigFile()) pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
return getPlugin(name, pluginDirs, rootcmd) if err != nil {
return nil, err
}
candidates, err := listPluginCandidates(pluginDirs)
if err != nil {
return nil, err
} }
func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugin, error) {
candidates := listPluginCandidates(pluginDirs)
if paths, ok := candidates[name]; ok { if paths, ok := candidates[name]; ok {
if len(paths) == 0 { if len(paths) == 0 {
return nil, errPluginNotFound(name) return nil, errPluginNotFound(name)
@ -137,21 +154,20 @@ func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugi
} }
// ListPlugins produces a list of the plugins available on the system // ListPlugins produces a list of the plugins available on the system
func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, error) { func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
pluginDirs := getPluginDirs(dockerCli.ConfigFile()) pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
candidates := listPluginCandidates(pluginDirs) if err != nil {
if len(candidates) == 0 { return nil, err
return nil, nil }
candidates, err := listPluginCandidates(pluginDirs)
if err != nil {
return nil, err
} }
var plugins []Plugin var plugins []Plugin
var mu sync.Mutex var mu sync.Mutex
ctx := rootcmd.Context() eg, _ := errgroup.WithContext(context.TODO())
if ctx == nil {
// Fallback, mostly for tests that pass a bare cobra.command
ctx = context.Background()
}
eg, _ := errgroup.WithContext(ctx)
cmds := rootcmd.Commands() cmds := rootcmd.Commands()
for _, paths := range candidates { for _, paths := range candidates {
func(paths []string) { func(paths []string) {
@ -188,7 +204,7 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e
// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin. // PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts. // The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
// The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow. // The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Command) (*exec.Cmd, error) { func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
// This uses the full original args, not the args which may // This uses the full original args, not the args which may
// have been provided by cobra to our caller. This is because // have been provided by cobra to our caller. This is because
// they lack e.g. global options which we must propagate here. // they lack e.g. global options which we must propagate here.
@ -198,8 +214,11 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
// fallback to their "invalid" command path. // fallback to their "invalid" command path.
return nil, errPluginNotFound(name) return nil, errPluginNotFound(name)
} }
exename := addExeSuffix(metadata.NamePrefix + name) exename := addExeSuffix(NamePrefix + name)
pluginDirs := getPluginDirs(dockerCli.ConfigFile()) pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
if err != nil {
return nil, err
}
for _, d := range pluginDirs { for _, d := range pluginDirs {
path := filepath.Join(d, exename) path := filepath.Join(d, exename)
@ -221,8 +240,7 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
// TODO: why are we not returning plugin.Err? // TODO: why are we not returning plugin.Err?
return nil, errPluginNotFound(name) return nil, errPluginNotFound(name)
} }
cmd := exec.Command(plugin.Path, args...) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments" cmd := exec.Command(plugin.Path, args...)
// Using dockerCli.{In,Out,Err}() here results in a hang until something is input. // Using dockerCli.{In,Out,Err}() here results in a hang until something is input.
// See: - https://github.com/golang/go/issues/10338 // See: - https://github.com/golang/go/issues/10338
// - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab // - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab
@ -232,7 +250,7 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Env = append(cmd.Environ(), metadata.ReexecEnvvar+"="+os.Args[0]) cmd.Env = append(cmd.Environ(), ReexecEnvvar+"="+os.Args[0])
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin) cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
return cmd, nil return cmd, nil
@ -242,5 +260,5 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
// IsPluginCommand checks if the given cmd is a plugin-stub. // IsPluginCommand checks if the given cmd is a plugin-stub.
func IsPluginCommand(cmd *cobra.Command) bool { func IsPluginCommand(cmd *cobra.Command) bool {
return cmd.Annotations[metadata.CommandAnnotationPlugin] == "true" return cmd.Annotations[CommandAnnotationPlugin] == "true"
} }

View File

@ -1,7 +1,6 @@
package manager package manager
import ( import (
"path/filepath"
"strings" "strings"
"testing" "testing"
@ -52,7 +51,8 @@ func TestListPluginCandidates(t *testing.T) {
dirs = append(dirs, dir.Join(d)) dirs = append(dirs, dir.Join(d))
} }
candidates := listPluginCandidates(dirs) candidates, err := listPluginCandidates(dirs)
assert.NilError(t, err)
exp := map[string][]string{ exp := map[string][]string{
"plugin1": { "plugin1": {
dir.Join("plugins1", "docker-plugin1"), dir.Join("plugins1", "docker-plugin1"),
@ -82,35 +82,6 @@ func TestListPluginCandidates(t *testing.T) {
assert.DeepEqual(t, candidates, exp) assert.DeepEqual(t, candidates, exp)
} }
func TestListPluginCandidatesEmpty(t *testing.T) {
tmpDir := t.TempDir()
candidates := listPluginCandidates([]string{tmpDir, filepath.Join(tmpDir, "no-such-dir")})
assert.Assert(t, len(candidates) == 0)
}
// Regression test for https://github.com/docker/cli/issues/5643.
// Check that inaccessible directories that come before accessible ones are ignored
// and do not prevent the latter from being processed.
func TestListPluginCandidatesInaccesibleDir(t *testing.T) {
dir := fs.NewDir(t, t.Name(),
fs.WithDir("no-perm", fs.WithMode(0)),
fs.WithDir("plugins",
fs.WithFile("docker-buildx", ""),
),
)
defer dir.Remove()
candidates := listPluginCandidates([]string{
dir.Join("no-perm"),
dir.Join("plugins"),
})
assert.DeepEqual(t, candidates, map[string][]string{
"buildx": {
dir.Join("plugins", "docker-buildx"),
},
})
}
func TestGetPlugin(t *testing.T) { func TestGetPlugin(t *testing.T) {
dir := fs.NewDir(t, t.Name(), dir := fs.NewDir(t, t.Name(),
fs.WithFile("docker-bbb", ` fs.WithFile("docker-bbb", `
@ -173,11 +144,14 @@ func TestErrPluginNotFound(t *testing.T) {
func TestGetPluginDirs(t *testing.T) { func TestGetPluginDirs(t *testing.T) {
cli := test.NewFakeCli(nil) cli := test.NewFakeCli(nil)
pluginDir := filepath.Join(config.Dir(), "cli-plugins") pluginDir, err := config.Path("cli-plugins")
assert.NilError(t, err)
expected := append([]string{pluginDir}, defaultSystemPluginDirs...) expected := append([]string{pluginDir}, defaultSystemPluginDirs...)
pluginDirs := getPluginDirs(cli.ConfigFile()) var pluginDirs []string
pluginDirs, err = getPluginDirs(cli.ConfigFile())
assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":")) assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))
assert.NilError(t, err)
extras := []string{ extras := []string{
"foo", "bar", "baz", "foo", "bar", "baz",
@ -186,6 +160,7 @@ func TestGetPluginDirs(t *testing.T) {
cli.SetConfigFile(&configfile.ConfigFile{ cli.SetConfigFile(&configfile.ConfigFile{
CLIPluginsExtraDirs: extras, CLIPluginsExtraDirs: extras,
}) })
pluginDirs = getPluginDirs(cli.ConfigFile()) pluginDirs, err = getPluginDirs(cli.ConfigFile())
assert.DeepEqual(t, expected, pluginDirs) assert.DeepEqual(t, expected, pluginDirs)
assert.NilError(t, err)
} }

View File

@ -1,23 +1,30 @@
package manager package manager
import (
"github.com/docker/cli/cli-plugins/metadata"
)
const ( const (
// NamePrefix is the prefix required on all plugin binary names // NamePrefix is the prefix required on all plugin binary names
NamePrefix = metadata.NamePrefix NamePrefix = "docker-"
// MetadataSubcommandName is the name of the plugin subcommand // MetadataSubcommandName is the name of the plugin subcommand
// which must be supported by every plugin and returns the // which must be supported by every plugin and returns the
// plugin metadata. // plugin metadata.
MetadataSubcommandName = metadata.MetadataSubcommandName MetadataSubcommandName = "docker-cli-plugin-metadata"
// HookSubcommandName is the name of the plugin subcommand // HookSubcommandName is the name of the plugin subcommand
// which must be implemented by plugins declaring support // which must be implemented by plugins declaring support
// for hooks in their metadata. // for hooks in their metadata.
HookSubcommandName = metadata.HookSubcommandName HookSubcommandName = "docker-cli-plugin-hooks"
) )
// Metadata provided by the plugin. // Metadata provided by the plugin.
type Metadata = metadata.Metadata type Metadata struct {
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
SchemaVersion string `json:",omitempty"`
// Vendor is the name of the plugin vendor. Mandatory
Vendor string `json:",omitempty"`
// Version is the optional version of this plugin.
Version string `json:",omitempty"`
// ShortDescription should be suitable for a single line help message.
ShortDescription string `json:",omitempty"`
// URL is a pointer to the plugin's homepage.
URL string `json:",omitempty"`
}

View File

@ -3,23 +3,21 @@ package manager
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"github.com/docker/cli/cli-plugins/metadata" "github.com/pkg/errors"
"github.com/docker/cli/internal/lazyregexp"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var pluginNameRe = lazyregexp.New("^[a-z][a-z0-9]*$") var pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
// Plugin represents a potential plugin with all it's metadata. // Plugin represents a potential plugin with all it's metadata.
type Plugin struct { type Plugin struct {
metadata.Metadata Metadata
Name string `json:",omitempty"` Name string `json:",omitempty"`
Path string `json:",omitempty"` Path string `json:",omitempty"`
@ -46,18 +44,18 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
// which would fail here, so there are all real errors. // which would fail here, so there are all real errors.
fullname := filepath.Base(path) fullname := filepath.Base(path)
if fullname == "." { if fullname == "." {
return Plugin{}, fmt.Errorf("unable to determine basename of plugin candidate %q", path) return Plugin{}, errors.Errorf("unable to determine basename of plugin candidate %q", path)
} }
var err error var err error
if fullname, err = trimExeSuffix(fullname); err != nil { if fullname, err = trimExeSuffix(fullname); err != nil {
return Plugin{}, fmt.Errorf("plugin candidate %q: %w", path, err) return Plugin{}, errors.Wrapf(err, "plugin candidate %q", path)
} }
if !strings.HasPrefix(fullname, metadata.NamePrefix) { if !strings.HasPrefix(fullname, NamePrefix) {
return Plugin{}, fmt.Errorf("plugin candidate %q: does not have %q prefix", path, metadata.NamePrefix) return Plugin{}, errors.Errorf("plugin candidate %q: does not have %q prefix", path, NamePrefix)
} }
p := Plugin{ p := Plugin{
Name: strings.TrimPrefix(fullname, metadata.NamePrefix), Name: strings.TrimPrefix(fullname, NamePrefix),
Path: path, Path: path,
} }
@ -114,9 +112,9 @@ func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte,
return nil, wrapAsPluginError(err, "failed to marshall hook data") return nil, wrapAsPluginError(err, "failed to marshall hook data")
} }
pCmd := exec.CommandContext(ctx, p.Path, p.Name, metadata.HookSubcommandName, string(hDataBytes)) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments" pCmd := exec.CommandContext(ctx, p.Path, p.Name, HookSubcommandName, string(hDataBytes))
pCmd.Env = os.Environ() pCmd.Env = os.Environ()
pCmd.Env = append(pCmd.Env, metadata.ReexecEnvvar+"="+os.Args[0]) pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0])
hookCmdOutput, err := pCmd.Output() hookCmdOutput, err := pCmd.Output()
if err != nil { if err != nil {
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand") return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")

View File

@ -1,16 +1,22 @@
package manager package manager
import ( import (
"fmt"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/pkg/errors"
) )
// This is made slightly more complex due to needing to be case-insensitive. // This is made slightly more complex due to needing to be case insensitive.
func trimExeSuffix(s string) (string, error) { func trimExeSuffix(s string) (string, error) {
ext := filepath.Ext(s) ext := filepath.Ext(s)
if ext == "" || !strings.EqualFold(ext, ".exe") { if ext == "" {
return "", fmt.Errorf("path %q lacks required file extension (.exe)", s) return "", errors.Errorf("path %q lacks required file extension", s)
}
exe := ".exe"
if !strings.EqualFold(ext, exe) {
return "", errors.Errorf("path %q lacks required %q suffix", s, exe)
} }
return strings.TrimSuffix(s, ext), nil return strings.TrimSuffix(s, ext), nil
} }

View File

@ -1,85 +0,0 @@
package manager
import (
"fmt"
"os"
"strings"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/baggage"
)
const (
// resourceAttributesEnvVar is the name of the envvar that includes additional
// resource attributes for OTEL as defined in the [OpenTelemetry specification].
//
// [OpenTelemetry specification]: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration
resourceAttributesEnvVar = "OTEL_RESOURCE_ATTRIBUTES"
// dockerCLIAttributePrefix is the prefix for any docker cli OTEL attributes.
//
// It is a copy of the const defined in [command.dockerCLIAttributePrefix].
dockerCLIAttributePrefix = "docker.cli."
cobraCommandPath = attribute.Key("cobra.command_path")
)
func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
commandPath := cmd.Annotations[metadata.CommandAnnotationPluginCommandPath]
if commandPath == "" {
commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name)
}
attrSet := attribute.NewSet(
cobraCommandPath.String(commandPath),
)
kvs := make([]attribute.KeyValue, 0, attrSet.Len())
for iter := attrSet.Iter(); iter.Next(); {
attr := iter.Attribute()
kvs = append(kvs, attribute.KeyValue{
Key: dockerCLIAttributePrefix + attr.Key,
Value: attr.Value,
})
}
return attribute.NewSet(kvs...)
}
func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string {
if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 {
// Construct baggage members for each of the attributes.
// Ignore any failures as these aren't significant and
// represent an internal issue.
members := make([]baggage.Member, 0, attrs.Len())
for iter := attrs.Iter(); iter.Next(); {
attr := iter.Attribute()
m, err := baggage.NewMemberRaw(string(attr.Key), attr.Value.AsString())
if err != nil {
otel.Handle(err)
continue
}
members = append(members, m)
}
// Combine plugin added resource attributes with ones found in the environment
// variable. Our own attributes should be namespaced so there shouldn't be a
// conflict. We do not parse the environment variable because we do not want
// to handle errors in user configuration.
attrsSlice := make([]string, 0, 2)
if v := strings.TrimSpace(os.Getenv(resourceAttributesEnvVar)); v != "" {
attrsSlice = append(attrsSlice, v)
}
if b, err := baggage.New(members...); err != nil {
otel.Handle(err)
} else if b.Len() > 0 {
attrsSlice = append(attrsSlice, b.String())
}
if len(attrsSlice) > 0 {
env = append(env, resourceAttributesEnvVar+"="+strings.Join(attrsSlice, ","))
}
}
return env
}

View File

@ -1,28 +0,0 @@
package metadata
const (
// CommandAnnotationPlugin is added to every stub command added by
// AddPluginCommandStubs with the value "true" and so can be
// used to distinguish plugin stubs from regular commands.
CommandAnnotationPlugin = "com.docker.cli.plugin"
// CommandAnnotationPluginVendor is added to every stub command
// added by AddPluginCommandStubs and contains the vendor of
// that plugin.
CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor"
// CommandAnnotationPluginVersion is added to every stub command
// added by AddPluginCommandStubs and contains the version of
// that plugin.
CommandAnnotationPluginVersion = "com.docker.cli.plugin.version"
// CommandAnnotationPluginInvalid is added to any stub command
// added by AddPluginCommandStubs for an invalid command (that
// is, one which failed it's candidate test) and contains the
// reason for the failure.
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
// CommandAnnotationPluginCommandPath is added to overwrite the
// command path for a plugin invocation.
CommandAnnotationPluginCommandPath = "com.docker.cli.plugin.command_path"
)

View File

@ -1,36 +0,0 @@
package metadata
const (
// NamePrefix is the prefix required on all plugin binary names
NamePrefix = "docker-"
// MetadataSubcommandName is the name of the plugin subcommand
// which must be supported by every plugin and returns the
// plugin metadata.
MetadataSubcommandName = "docker-cli-plugin-metadata"
// HookSubcommandName is the name of the plugin subcommand
// which must be implemented by plugins declaring support
// for hooks in their metadata.
HookSubcommandName = "docker-cli-plugin-hooks"
// ReexecEnvvar is the name of an ennvar which is set to the command
// used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI.
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
)
// Metadata provided by the plugin.
type Metadata struct {
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
SchemaVersion string `json:",omitempty"`
// Vendor is the name of the plugin vendor. Mandatory
Vendor string `json:",omitempty"`
// Version is the optional version of this plugin.
Version string `json:",omitempty"`
// ShortDescription should be suitable for a single line help message.
ShortDescription string `json:",omitempty"`
// URL is a pointer to the plugin's homepage.
URL string `json:",omitempty"`
}

View File

@ -3,18 +3,17 @@ package plugin
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"sync" "sync"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/socket" "github.com/docker/cli/cli-plugins/socket"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/connhelper" "github.com/docker/cli/cli/connhelper"
"github.com/docker/cli/cli/debug" "github.com/docker/cli/cli/debug"
"github.com/moby/moby/client" "github.com/docker/docker/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
) )
@ -30,12 +29,12 @@ import (
var PersistentPreRunE func(*cobra.Command, []string) error var PersistentPreRunE func(*cobra.Command, []string) error
// RunPlugin executes the specified plugin command // RunPlugin executes the specified plugin command
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) error { func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) error {
tcmd := newPluginCommand(dockerCli, plugin, meta) tcmd := newPluginCommand(dockerCli, plugin, meta)
var persistentPreRunOnce sync.Once var persistentPreRunOnce sync.Once
PersistentPreRunE = func(cmd *cobra.Command, _ []string) error { PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
var retErr error var err error
persistentPreRunOnce.Do(func() { persistentPreRunOnce.Do(func() {
ctx, cancel := context.WithCancel(cmd.Context()) ctx, cancel := context.WithCancel(cmd.Context())
cmd.SetContext(ctx) cmd.SetContext(ctx)
@ -47,7 +46,7 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
opts = append(opts, withPluginClientConn(plugin.Name())) opts = append(opts, withPluginClientConn(plugin.Name()))
} }
opts = append(opts, command.WithEnableGlobalMeterProvider(), command.WithEnableGlobalTracerProvider()) opts = append(opts, command.WithEnableGlobalMeterProvider(), command.WithEnableGlobalTracerProvider())
retErr = tcmd.Initialize(opts...) err = tcmd.Initialize(opts...)
ogRunE := cmd.RunE ogRunE := cmd.RunE
if ogRunE == nil { if ogRunE == nil {
ogRun := cmd.Run ogRun := cmd.Run
@ -67,7 +66,7 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
return err return err
} }
}) })
return retErr return err
} }
cmd, args, err := tcmd.HandleGlobalFlags() cmd, args, err := tcmd.HandleGlobalFlags()
@ -81,7 +80,7 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
} }
// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function. // Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) { func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
otel.SetErrorHandler(debug.OTELErrorHandler) otel.SetErrorHandler(debug.OTELErrorHandler)
dockerCli, err := command.NewDockerCli() dockerCli, err := command.NewDockerCli()
@ -93,25 +92,26 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) {
plugin := makeCmd(dockerCli) plugin := makeCmd(dockerCli)
if err := RunPlugin(dockerCli, plugin, meta); err != nil { if err := RunPlugin(dockerCli, plugin, meta); err != nil {
var stErr cli.StatusError if sterr, ok := err.(cli.StatusError); ok {
if errors.As(err, &stErr) { if sterr.Status != "" {
fmt.Fprintln(dockerCli.Err(), sterr.Status)
}
// StatusError should only be used for errors, and all errors should // StatusError should only be used for errors, and all errors should
// have a non-zero exit status, so never exit with 0 // have a non-zero exit status, so never exit with 0
if stErr.StatusCode == 0 { // FIXME(thaJeztah): this should never be used with a zero status-code. Check if we do this anywhere. if sterr.StatusCode == 0 {
stErr.StatusCode = 1 os.Exit(1)
} }
_, _ = fmt.Fprintln(dockerCli.Err(), stErr) os.Exit(sterr.StatusCode)
os.Exit(stErr.StatusCode)
} }
_, _ = fmt.Fprintln(dockerCli.Err(), err) fmt.Fprintln(dockerCli.Err(), err)
os.Exit(1) os.Exit(1)
} }
} }
func withPluginClientConn(name string) command.CLIOption { func withPluginClientConn(name string) command.CLIOption {
return func(cli *command.DockerCli) error { return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) {
cmd := "docker" cmd := "docker"
if x := os.Getenv(metadata.ReexecEnvvar); x != "" { if x := os.Getenv(manager.ReexecEnvvar); x != "" {
cmd = x cmd = x
} }
var flags []string var flags []string
@ -133,19 +133,16 @@ func withPluginClientConn(name string) command.CLIOption {
helper, err := connhelper.GetCommandConnectionHelper(cmd, flags...) helper, err := connhelper.GetCommandConnectionHelper(cmd, flags...)
if err != nil { if err != nil {
return err return nil, err
}
apiClient, err := client.NewClientWithOpts(client.WithDialContext(helper.Dialer))
if err != nil {
return err
}
return command.WithAPIClient(apiClient)(cli)
}
} }
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) *cli.TopLevelCommand { return client.NewClientWithOpts(client.WithDialContext(helper.Dialer))
})
}
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cli.TopLevelCommand {
name := plugin.Name() name := plugin.Name()
fullname := metadata.NamePrefix + name fullname := manager.NamePrefix + name
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name), Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name),
@ -161,7 +158,7 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
CompletionOptions: cobra.CompletionOptions{ CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: false, DisableDefaultCmd: false,
HiddenDefaultCmd: true, HiddenDefaultCmd: true,
DisableDescriptions: os.Getenv("DOCKER_CLI_DISABLE_COMPLETION_DESCRIPTION") != "", DisableDescriptions: true,
}, },
} }
opts, _ := cli.SetupPluginRootCommand(cmd) opts, _ := cli.SetupPluginRootCommand(cmd)
@ -180,12 +177,12 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags()) return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags())
} }
func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra.Command { func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
if meta.ShortDescription == "" { if meta.ShortDescription == "" {
meta.ShortDescription = plugin.Short meta.ShortDescription = plugin.Short
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: metadata.MetadataSubcommandName, Use: manager.MetadataSubcommandName,
Hidden: true, Hidden: true,
// Suppress the global/parent PersistentPreRunE, which // Suppress the global/parent PersistentPreRunE, which
// needlessly initializes the client and tries to // needlessly initializes the client and tries to
@ -203,8 +200,8 @@ func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra
// RunningStandalone tells a CLI plugin it is run standalone by direct execution // RunningStandalone tells a CLI plugin it is run standalone by direct execution
func RunningStandalone() bool { func RunningStandalone() bool {
if os.Getenv(metadata.ReexecEnvvar) != "" { if os.Getenv(manager.ReexecEnvvar) != "" {
return false return false
} }
return len(os.Args) < 2 || os.Args[1] != metadata.MetadataSubcommandName return len(os.Args) < 2 || os.Args[1] != manager.MetadataSubcommandName
} }

View File

@ -178,39 +178,24 @@ func TestConnectAndWait(t *testing.T) {
// TODO: this test cannot be executed with `t.Parallel()`, due to // TODO: this test cannot be executed with `t.Parallel()`, due to
// relying on goroutine numbers to ensure correct behaviour // relying on goroutine numbers to ensure correct behaviour
t.Run("connect goroutine exits after EOF", func(t *testing.T) { t.Run("connect goroutine exits after EOF", func(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
srv, err := NewPluginServer(nil) srv, err := NewPluginServer(nil)
assert.NilError(t, err, "failed to setup server") assert.NilError(t, err, "failed to setup server")
defer srv.Close() defer srv.Close()
t.Setenv(EnvKey, srv.Addr().String()) t.Setenv(EnvKey, srv.Addr().String())
runtime.Gosched()
numGoroutines := runtime.NumGoroutine() numGoroutines := runtime.NumGoroutine()
ConnectAndWait(func() {}) ConnectAndWait(func() {})
assert.Equal(t, runtime.NumGoroutine(), numGoroutines+1)
runtime.Gosched()
poll.WaitOn(t, func(t poll.LogT) poll.Result {
// +1 goroutine for the poll.WaitOn
// +1 goroutine for the connect goroutine
if runtime.NumGoroutine() < numGoroutines+1+1 {
return poll.Continue("waiting for connect goroutine to spawn")
}
return poll.Success()
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(500*time.Millisecond))
srv.Close() srv.Close()
runtime.Gosched()
poll.WaitOn(t, func(t poll.LogT) poll.Result { poll.WaitOn(t, func(t poll.LogT) poll.Result {
// +1 goroutine for the poll.WaitOn
if runtime.NumGoroutine() > numGoroutines+1 { if runtime.NumGoroutine() > numGoroutines+1 {
return poll.Continue("waiting for connect goroutine to exit") return poll.Continue("waiting for connect goroutine to exit")
} }
return poll.Success() return poll.Success()
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(500*time.Millisecond)) }, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(10*time.Millisecond))
}) })
} }

View File

@ -3,12 +3,15 @@ package cli
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"sort" "sort"
"strings" "strings"
"github.com/docker/cli/cli-plugins/metadata" pluginmanager "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
cliflags "github.com/docker/cli/cli/flags" cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/docker/pkg/homedir"
"github.com/docker/docker/registry"
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
"github.com/moby/term" "github.com/moby/term"
"github.com/morikuni/aec" "github.com/morikuni/aec"
@ -59,6 +62,13 @@ func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *c
"docs.code-delimiter": `"`, // https://github.com/docker/cli-docs-tool/blob/77abede22166eaea4af7335096bdcedd043f5b19/annotation/annotation.go#L20-L22 "docs.code-delimiter": `"`, // https://github.com/docker/cli-docs-tool/blob/77abede22166eaea4af7335096bdcedd043f5b19/annotation/annotation.go#L20-L22
} }
// Configure registry.CertsDir() when running in rootless-mode
if os.Getenv("ROOTLESSKIT_STATE_DIR") != "" {
if configHome, err := homedir.GetConfigHome(); err == nil {
registry.SetCertsDir(filepath.Join(configHome, "docker/certs.d"))
}
}
return opts, helpCommand return opts, helpCommand
} }
@ -82,8 +92,12 @@ func FlagErrorFunc(cmd *cobra.Command, err error) error {
return nil return nil
} }
usage := ""
if cmd.HasSubCommands() {
usage = "\n\n" + cmd.UsageString()
}
return StatusError{ return StatusError{
Status: fmt.Sprintf("%s\n\nUsage: %s\n\nRun '%s --help' for more information", err, cmd.UseLine(), cmd.CommandPath()), Status: fmt.Sprintf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage),
StatusCode: 125, StatusCode: 125,
} }
} }
@ -242,7 +256,7 @@ func hasAdditionalHelp(cmd *cobra.Command) bool {
} }
func isPlugin(cmd *cobra.Command) bool { func isPlugin(cmd *cobra.Command) bool {
return cmd.Annotations[metadata.CommandAnnotationPlugin] == "true" return pluginmanager.IsPluginCommand(cmd)
} }
func hasAliases(cmd *cobra.Command) bool { func hasAliases(cmd *cobra.Command) bool {
@ -327,10 +341,8 @@ func operationSubCommands(cmd *cobra.Command) []*cobra.Command {
return cmds return cmds
} }
const defaultTermWidth = 80
func wrappedFlagUsages(cmd *cobra.Command) string { func wrappedFlagUsages(cmd *cobra.Command) string {
width := defaultTermWidth width := 80
if ws, err := term.GetWinsize(0); err == nil { if ws, err := term.GetWinsize(0); err == nil {
width = int(ws.Width) width = int(ws.Width)
} }
@ -346,9 +358,9 @@ func decoratedName(cmd *cobra.Command) string {
} }
func vendorAndVersion(cmd *cobra.Command) string { func vendorAndVersion(cmd *cobra.Command) string {
if vendor, ok := cmd.Annotations[metadata.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) { if vendor, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) {
version := "" version := ""
if v, ok := cmd.Annotations[metadata.CommandAnnotationPluginVersion]; ok && v != "" { if v, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVersion]; ok && v != "" {
version = ", " + v version = ", " + v
} }
return fmt.Sprintf("(%s%s)", vendor, version) return fmt.Sprintf("(%s%s)", vendor, version)
@ -407,7 +419,7 @@ func invalidPlugins(cmd *cobra.Command) []*cobra.Command {
} }
func invalidPluginReason(cmd *cobra.Command) string { func invalidPluginReason(cmd *cobra.Command) string {
return cmd.Annotations[metadata.CommandAnnotationPluginInvalid] return cmd.Annotations[pluginmanager.CommandAnnotationPluginInvalid]
} }
const usageTemplate = `Usage: const usageTemplate = `Usage:
@ -510,4 +522,4 @@ Run '{{.CommandPath}} COMMAND --help' for more information on a command.
` `
const helpTemplate = ` const helpTemplate = `
{{- if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` {{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`

View File

@ -3,7 +3,7 @@ package cli
import ( import (
"testing" "testing"
"github.com/docker/cli/cli-plugins/metadata" pluginmanager "github.com/docker/cli/cli-plugins/manager"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
@ -49,9 +49,9 @@ func TestVendorAndVersion(t *testing.T) {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "test", Use: "test",
Annotations: map[string]string{ Annotations: map[string]string{
metadata.CommandAnnotationPlugin: "true", pluginmanager.CommandAnnotationPlugin: "true",
metadata.CommandAnnotationPluginVendor: tc.vendor, pluginmanager.CommandAnnotationPluginVendor: tc.vendor,
metadata.CommandAnnotationPluginVersion: tc.version, pluginmanager.CommandAnnotationPluginVersion: tc.version,
}, },
} }
assert.Equal(t, vendorAndVersion(cmd), tc.expected) assert.Equal(t, vendorAndVersion(cmd), tc.expected)
@ -69,8 +69,8 @@ func TestInvalidPlugin(t *testing.T) {
assert.Assert(t, is.Len(invalidPlugins(root), 0)) assert.Assert(t, is.Len(invalidPlugins(root), 0))
sub1.Annotations = map[string]string{ sub1.Annotations = map[string]string{
metadata.CommandAnnotationPlugin: "true", pluginmanager.CommandAnnotationPlugin: "true",
metadata.CommandAnnotationPluginInvalid: "foo", pluginmanager.CommandAnnotationPluginInvalid: "foo",
} }
root.AddCommand(sub1, sub2) root.AddCommand(sub1, sub2)
sub1.AddCommand(sub1sub1, sub1sub2) sub1.AddCommand(sub1sub1, sub1sub2)
@ -100,6 +100,6 @@ func TestDecoratedName(t *testing.T) {
topLevelCommand := &cobra.Command{Use: "pluginTopLevelCommand"} topLevelCommand := &cobra.Command{Use: "pluginTopLevelCommand"}
root.AddCommand(topLevelCommand) root.AddCommand(topLevelCommand)
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand ") assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand ")
topLevelCommand.Annotations = map[string]string{metadata.CommandAnnotationPlugin: "true"} topLevelCommand.Annotations = map[string]string{pluginmanager.CommandAnnotationPlugin: "true"}
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand*") assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand*")
} }

View File

@ -3,16 +3,16 @@ package builder
import ( import (
"context" "context"
"github.com/moby/moby/api/types/build" "github.com/docker/docker/api/types"
"github.com/moby/moby/client" "github.com/docker/docker/client"
) )
type fakeClient struct { type fakeClient struct {
client.Client client.Client
builderPruneFunc func(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) builderPruneFunc func(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error)
} }
func (c *fakeClient) BuildCachePrune(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) { func (c *fakeClient) BuildCachePrune(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
if c.builderPruneFunc != nil { if c.builderPruneFunc != nil {
return c.builderPruneFunc(ctx, opts) return c.builderPruneFunc(ctx, opts)
} }

View File

@ -23,22 +23,3 @@ func NewBuilderCommand(dockerCli command.Cli) *cobra.Command {
) )
return cmd return cmd
} }
// NewBakeStubCommand returns a cobra command "stub" for the "bake" subcommand.
// This command is a placeholder / stub that is dynamically replaced by an
// alias for "docker buildx bake" if BuildKit is enabled (and the buildx plugin
// installed).
func NewBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
return &cobra.Command{
Use: "bake [OPTIONS] [TARGET...]",
Short: "Build from a file",
RunE: command.ShowHelp(dockerCLI.Err()),
Annotations: map[string]string{
// We want to show this command in the "top" category in --help
// output, and not to be grouped under "management commands".
"category-top": "5",
"aliases": "docker buildx bake",
"version": "1.31",
},
}
}

View File

@ -9,10 +9,10 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/internal/prompt"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/docker/go-units" "github.com/docker/docker/api/types"
"github.com/moby/moby/api/types/build" "github.com/docker/docker/errdefs"
units "github.com/docker/go-units"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -69,18 +69,18 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
warning = allCacheWarning warning = allCacheWarning
} }
if !options.force { if !options.force {
r, err := prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), warning) r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
if err != nil { if err != nil {
return 0, "", err return 0, "", err
} }
if !r { if !r {
return 0, "", cancelledErr{errors.New("builder prune has been cancelled")} return 0, "", errdefs.Cancelled(errors.New("builder prune has been cancelled"))
} }
} }
report, err := dockerCli.Client().BuildCachePrune(ctx, build.CachePruneOptions{ report, err := dockerCli.Client().BuildCachePrune(ctx, types.BuildCachePruneOptions{
All: options.all, All: options.all,
KeepStorage: options.keepStorage.Value(), // FIXME(thaJeztah): rewrite to use new options; see https://github.com/moby/moby/pull/48720 KeepStorage: options.keepStorage.Value(),
Filters: pruneFilters, Filters: pruneFilters,
}) })
if err != nil { if err != nil {
@ -100,10 +100,6 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
return report.SpaceReclaimed, output, nil return report.SpaceReclaimed, output, nil
} }
type cancelledErr struct{ error }
func (cancelledErr) Cancelled() {}
// CachePrune executes a prune command for build cache // CachePrune executes a prune command for build cache
func CachePrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) { func CachePrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter}) return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter})

View File

@ -7,7 +7,7 @@ import (
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/build" "github.com/docker/docker/api/types"
) )
func TestBuilderPromptTermination(t *testing.T) { func TestBuilderPromptTermination(t *testing.T) {
@ -15,7 +15,7 @@ func TestBuilderPromptTermination(t *testing.T) {
t.Cleanup(cancel) t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
builderPruneFunc: func(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) { builderPruneFunc: func(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
return nil, errors.New("fakeClient builderPruneFunc should not be called") return nil, errors.New("fakeClient builderPruneFunc should not be called")
}, },
}) })

View File

@ -3,8 +3,8 @@ package checkpoint
import ( import (
"context" "context"
"github.com/moby/moby/api/types/checkpoint" "github.com/docker/docker/api/types/checkpoint"
"github.com/moby/moby/client" "github.com/docker/docker/client"
) )
type fakeClient struct { type fakeClient struct {

View File

@ -7,7 +7,7 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/moby/moby/api/types/checkpoint" "github.com/docker/docker/api/types/checkpoint"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -40,8 +40,8 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runCreate(ctx context.Context, dockerCLI command.Cli, opts createOptions) error { func runCreate(ctx context.Context, dockerCli command.Cli, opts createOptions) error {
err := dockerCLI.Client().CheckpointCreate(ctx, opts.container, checkpoint.CreateOptions{ err := dockerCli.Client().CheckpointCreate(ctx, opts.container, checkpoint.CreateOptions{
CheckpointID: opts.checkpoint, CheckpointID: opts.checkpoint,
CheckpointDir: opts.checkpointDir, CheckpointDir: opts.checkpointDir,
Exit: !opts.leaveRunning, Exit: !opts.leaveRunning,
@ -50,6 +50,6 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, opts createOptions) e
return err return err
} }
_, _ = fmt.Fprintln(dockerCLI.Out(), opts.checkpoint) fmt.Fprintf(dockerCli.Out(), "%s\n", opts.checkpoint)
return nil return nil
} }

View File

@ -1,14 +1,13 @@
package checkpoint package checkpoint
import ( import (
"errors"
"io" "io"
"strconv"
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/checkpoint" "github.com/docker/docker/api/types/checkpoint"
"github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
) )
@ -21,16 +20,16 @@ func TestCheckpointCreateErrors(t *testing.T) {
}{ }{
{ {
args: []string{"too-few-arguments"}, args: []string{"too-few-arguments"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {
args: []string{"too", "many", "arguments"}, args: []string{"too", "many", "arguments"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error { checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
return errors.New("error creating checkpoint for container foo") return errors.Errorf("error creating checkpoint for container foo")
}, },
expectedError: "error creating checkpoint for container foo", expectedError: "error creating checkpoint for container foo",
}, },
@ -49,39 +48,26 @@ func TestCheckpointCreateErrors(t *testing.T) {
} }
func TestCheckpointCreateWithOptions(t *testing.T) { func TestCheckpointCreateWithOptions(t *testing.T) {
const ( var containerID, checkpointID, checkpointDir string
containerName = "container-foo" var exit bool
checkpointName = "checkpoint-bar"
checkpointDir = "/dir/foo"
)
for _, tc := range []bool{true, false} {
leaveRunning := strconv.FormatBool(tc)
t.Run("leave-running="+leaveRunning, func(t *testing.T) {
var actualContainerName string
var actualOptions checkpoint.CreateOptions
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error { checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
actualContainerName = container containerID = container
actualOptions = options checkpointID = options.CheckpointID
checkpointDir = options.CheckpointDir
exit = options.Exit
return nil return nil
}, },
}) })
cmd := newCreateCommand(cli) cmd := newCreateCommand(cli)
cmd.SetOut(io.Discard) cp := "checkpoint-bar"
cmd.SetErr(io.Discard) cmd.SetArgs([]string{"container-foo", cp})
cmd.SetArgs([]string{containerName, checkpointName}) cmd.Flags().Set("leave-running", "true")
assert.Check(t, cmd.Flags().Set("leave-running", leaveRunning)) cmd.Flags().Set("checkpoint-dir", "/dir/foo")
assert.Check(t, cmd.Flags().Set("checkpoint-dir", checkpointDir))
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
assert.Check(t, is.Equal(actualContainerName, containerName)) assert.Check(t, is.Equal("container-foo", containerID))
expected := checkpoint.CreateOptions{ assert.Check(t, is.Equal(cp, checkpointID))
CheckpointID: checkpointName, assert.Check(t, is.Equal("/dir/foo", checkpointDir))
CheckpointDir: checkpointDir, assert.Check(t, is.Equal(false, exit))
Exit: !tc, assert.Check(t, is.Equal(cp, strings.TrimSpace(cli.OutBuffer().String())))
}
assert.Check(t, is.Equal(actualOptions, expected))
assert.Check(t, is.Equal(strings.TrimSpace(cli.OutBuffer().String()), checkpointName))
})
}
} }

View File

@ -2,7 +2,7 @@ package checkpoint
import ( import (
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/checkpoint" "github.com/docker/docker/api/types/checkpoint"
) )
const ( const (

View File

@ -5,7 +5,7 @@ import (
"testing" "testing"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/checkpoint" "github.com/docker/docker/api/types/checkpoint"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
) )

View File

@ -7,7 +7,7 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/checkpoint" "github.com/docker/docker/api/types/checkpoint"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )

View File

@ -1,12 +1,12 @@
package checkpoint package checkpoint
import ( import (
"errors"
"io" "io"
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/checkpoint" "github.com/docker/docker/api/types/checkpoint"
"github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
@ -20,16 +20,16 @@ func TestCheckpointListErrors(t *testing.T) {
}{ }{
{ {
args: []string{}, args: []string{},
expectedError: "requires 1 argument", expectedError: "requires exactly 1 argument",
}, },
{ {
args: []string{"too", "many", "arguments"}, args: []string{"too", "many", "arguments"},
expectedError: "requires 1 argument", expectedError: "requires exactly 1 argument",
}, },
{ {
args: []string{"foo"}, args: []string{"foo"},
checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) { checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
return []checkpoint.Summary{}, errors.New("error getting checkpoints for container foo") return []checkpoint.Summary{}, errors.Errorf("error getting checkpoints for container foo")
}, },
expectedError: "error getting checkpoints for container foo", expectedError: "error getting checkpoints for container foo",
}, },

View File

@ -5,7 +5,7 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/moby/moby/api/types/checkpoint" "github.com/docker/docker/api/types/checkpoint"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )

View File

@ -1,12 +1,12 @@
package checkpoint package checkpoint
import ( import (
"errors"
"io" "io"
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/checkpoint" "github.com/docker/docker/api/types/checkpoint"
"github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
) )
@ -19,16 +19,16 @@ func TestCheckpointRemoveErrors(t *testing.T) {
}{ }{
{ {
args: []string{"too-few-arguments"}, args: []string{"too-few-arguments"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {
args: []string{"too", "many", "arguments"}, args: []string{"too", "many", "arguments"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},
checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error { checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
return errors.New("error deleting checkpoint") return errors.Errorf("error deleting checkpoint")
}, },
expectedError: "error deleting checkpoint", expectedError: "error deleting checkpoint",
}, },

View File

@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.23 //go:build go1.21
package command package command
@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"sync" "sync"
@ -20,14 +21,21 @@ import (
"github.com/docker/cli/cli/context/store" "github.com/docker/cli/cli/context/store"
"github.com/docker/cli/cli/debug" "github.com/docker/cli/cli/debug"
cliflags "github.com/docker/cli/cli/flags" cliflags "github.com/docker/cli/cli/flags"
manifeststore "github.com/docker/cli/cli/manifest/store"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/cli/version" "github.com/docker/cli/cli/version"
dopts "github.com/docker/cli/opts" dopts "github.com/docker/cli/opts"
"github.com/moby/moby/api/types/build" "github.com/docker/docker/api"
"github.com/moby/moby/api/types/swarm" "github.com/docker/docker/api/types"
"github.com/moby/moby/client" "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/docker/go-connections/tlsconfig"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
notaryclient "github.com/theupdateframework/notary/client"
) )
const defaultInitTimeout = 2 * time.Second const defaultInitTimeout = 2 * time.Second
@ -45,10 +53,13 @@ type Cli interface {
Streams Streams
SetIn(in *streams.In) SetIn(in *streams.In)
Apply(ops ...CLIOption) error Apply(ops ...CLIOption) error
config.Provider ConfigFile() *configfile.ConfigFile
ServerInfo() ServerInfo ServerInfo() ServerInfo
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
DefaultVersion() string DefaultVersion() string
CurrentVersion() string CurrentVersion() string
ManifestStore() manifeststore.Store
RegistryClient(bool) registryclient.RegistryClient
ContentTrustEnabled() bool ContentTrustEnabled() bool
BuildKitEnabled() (bool, error) BuildKitEnabled() (bool, error)
ContextStore() store.Store ContextStore() store.Store
@ -58,9 +69,7 @@ type Cli interface {
} }
// DockerCli is an instance the docker command line client. // DockerCli is an instance the docker command line client.
// Instances of the client should be created using the [NewDockerCli] // Instances of the client can be returned from NewDockerCli.
// constructor to make sure they are properly initialized with defaults
// set.
type DockerCli struct { type DockerCli struct {
configFile *configfile.ConfigFile configFile *configfile.ConfigFile
options *cliflags.ClientOptions options *cliflags.ClientOptions
@ -75,7 +84,7 @@ type DockerCli struct {
init sync.Once init sync.Once
initErr error initErr error
dockerEndpoint docker.Endpoint dockerEndpoint docker.Endpoint
contextStoreConfig *store.Config contextStoreConfig store.Config
initTimeout time.Duration initTimeout time.Duration
res telemetryResource res telemetryResource
@ -87,9 +96,9 @@ type DockerCli struct {
enableGlobalMeter, enableGlobalTracer bool enableGlobalMeter, enableGlobalTracer bool
} }
// DefaultVersion returns [client.DefaultAPIVersion]. // DefaultVersion returns api.defaultVersion.
func (*DockerCli) DefaultVersion() string { func (cli *DockerCli) DefaultVersion() string {
return client.DefaultAPIVersion return api.DefaultVersion
} }
// CurrentVersion returns the API version currently negotiated, or the default // CurrentVersion returns the API version currently negotiated, or the default
@ -97,7 +106,7 @@ func (*DockerCli) DefaultVersion() string {
func (cli *DockerCli) CurrentVersion() string { func (cli *DockerCli) CurrentVersion() string {
_ = cli.initialize() _ = cli.initialize()
if cli.client == nil { if cli.client == nil {
return client.DefaultAPIVersion return api.DefaultVersion
} }
return cli.client.ClientVersion() return cli.client.ClientVersion()
} }
@ -105,7 +114,7 @@ func (cli *DockerCli) CurrentVersion() string {
// Client returns the APIClient // Client returns the APIClient
func (cli *DockerCli) Client() client.APIClient { func (cli *DockerCli) Client() client.APIClient {
if err := cli.initialize(); err != nil { if err := cli.initialize(); err != nil {
_, _ = fmt.Fprintln(cli.Err(), "Failed to initialize:", err) _, _ = fmt.Fprintf(cli.Err(), "Failed to initialize: %s\n", err)
os.Exit(1) os.Exit(1)
} }
return cli.client return cli.client
@ -179,7 +188,7 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
} }
si := cli.ServerInfo() si := cli.ServerInfo()
if si.BuildkitVersion == build.BuilderBuildKit { if si.BuildkitVersion == types.BuilderBuildKit {
// The daemon advertised BuildKit as the preferred builder; this may // The daemon advertised BuildKit as the preferred builder; this may
// be either a Linux daemon or a Windows daemon with experimental // be either a Linux daemon or a Windows daemon with experimental
// BuildKit support enabled. // BuildKit support enabled.
@ -193,16 +202,16 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
// HooksEnabled returns whether plugin hooks are enabled. // HooksEnabled returns whether plugin hooks are enabled.
func (cli *DockerCli) HooksEnabled() bool { func (cli *DockerCli) HooksEnabled() bool {
// use DOCKER_CLI_HOOKS env var value if set and not empty // legacy support DOCKER_CLI_HINTS env var
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" { if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
enabled, err := strconv.ParseBool(v) enabled, err := strconv.ParseBool(v)
if err != nil { if err != nil {
return false return false
} }
return enabled return enabled
} }
// legacy support DOCKER_CLI_HINTS env var // use DOCKER_CLI_HOOKS env var value if set and not empty
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" { if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
enabled, err := strconv.ParseBool(v) enabled, err := strconv.ParseBool(v)
if err != nil { if err != nil {
return false return false
@ -221,6 +230,30 @@ func (cli *DockerCli) HooksEnabled() bool {
return false return false
} }
// ManifestStore returns a store for local manifests
func (cli *DockerCli) ManifestStore() manifeststore.Store {
// TODO: support override default location from config file
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
}
// RegistryClient returns a client for communicating with a Docker distribution
// registry
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig {
return ResolveAuthConfig(cli.ConfigFile(), index)
}
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
}
// WithInitializeClient is passed to DockerCli.Initialize by callers who wish to set a particular API Client for use by the CLI.
func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) CLIOption {
return func(dockerCli *DockerCli) error {
var err error
dockerCli.client, err = makeClient(dockerCli)
return err
}
}
// Initialize the dockerCli runs initialization that must happen after command // Initialize the dockerCli runs initialization that must happen after command
// line flags are parsed. // line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error { func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error {
@ -239,36 +272,16 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
debug.Enable() debug.Enable()
} }
if opts.Context != "" && len(opts.Hosts) > 0 { if opts.Context != "" && len(opts.Hosts) > 0 {
return errors.New("conflicting options: cannot specify both --host and --context") return errors.New("conflicting options: either specify --host or --context, not both")
}
if cli.contextStoreConfig == nil {
// This path can be hit when calling Initialize on a DockerCli that's
// not constructed through [NewDockerCli]. Using the default context
// store without a config set will result in Endpoints from contexts
// not being type-mapped correctly, and used as a generic "map[string]any",
// instead of a [docker.EndpointMeta].
//
// When looking up the API endpoint (using [EndpointFromContext]), no
// endpoint will be found, and a default, empty endpoint will be used
// instead which in its turn, causes newAPIClientFromEndpoint to
// be initialized with the default config instead of settings for
// the current context (which may mean; connecting with the wrong
// endpoint and/or TLS Config to be missing).
//
// [EndpointFromContext]: https://github.com/docker/cli/blob/33494921b80fd0b5a06acc3a34fa288de4bb2e6b/cli/context/docker/load.go#L139-L149
if err := WithDefaultContextStoreConfig()(cli); err != nil {
return err
}
} }
cli.options = opts cli.options = opts
cli.configFile = config.LoadDefaultConfigFile(cli.err) cli.configFile = config.LoadDefaultConfigFile(cli.err)
cli.currentContext = resolveContextName(cli.options, cli.configFile) cli.currentContext = resolveContextName(cli.options, cli.configFile)
cli.contextStore = &ContextStoreWithDefault{ cli.contextStore = &ContextStoreWithDefault{
Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig), Store: store.New(config.ContextStoreDir(), cli.contextStoreConfig),
Resolver: func() (*DefaultContext, error) { Resolver: func() (*DefaultContext, error) {
return ResolveDefaultContext(cli.options, *cli.contextStoreConfig) return ResolveDefaultContext(cli.options, cli.contextStoreConfig)
}, },
} }
@ -279,7 +292,6 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
if cli.enableGlobalTracer { if cli.enableGlobalTracer {
cli.createGlobalTracerProvider(cli.baseCtx) cli.createGlobalTracerProvider(cli.baseCtx)
} }
filterResourceAttributesEnvvar()
return nil return nil
} }
@ -287,7 +299,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
// NewAPIClientFromFlags creates a new APIClient from command line flags // NewAPIClientFromFlags creates a new APIClient from command line flags
func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.ConfigFile) (client.APIClient, error) { func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
if opts.Context != "" && len(opts.Hosts) > 0 { if opts.Context != "" && len(opts.Hosts) > 0 {
return nil, errors.New("conflicting options: cannot specify both --host and --context") return nil, errors.New("conflicting options: either specify --host or --context, not both")
} }
storeConfig := DefaultContextStoreConfig() storeConfig := DefaultContextStoreConfig()
@ -333,10 +345,7 @@ func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint,
// Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags) // Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) { func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) {
// defaultToTLS determines whether we should use a TLS host as default host, err := getServerHost(opts.Hosts, opts.TLSOptions)
// if nothing was configured by the user.
defaultToTLS := opts.TLSOptions != nil
host, err := getServerHost(opts.Hosts, defaultToTLS)
if err != nil { if err != nil {
return docker.Endpoint{}, err return docker.Endpoint{}, err
} }
@ -394,6 +403,11 @@ func (cli *DockerCli) initializeFromClient() {
cli.client.NegotiateAPIVersionPing(ping) cli.client.NegotiateAPIVersionPing(ping)
} }
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
}
// ContextStore returns the ContextStore // ContextStore returns the ContextStore
func (cli *DockerCli) ContextStore() store.Store { func (cli *DockerCli) ContextStore() store.Store {
return cli.contextStore return cli.contextStore
@ -461,7 +475,7 @@ func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
if err := cli.initialize(); err != nil { if err := cli.initialize(); err != nil {
// Note that we're not terminating here, as this function may be used // Note that we're not terminating here, as this function may be used
// in cases where we're able to continue. // in cases where we're able to continue.
_, _ = fmt.Fprintln(cli.Err(), cli.initErr) _, _ = fmt.Fprintf(cli.Err(), "%v\n", cli.initErr)
} }
return cli.dockerEndpoint return cli.dockerEndpoint
} }
@ -509,7 +523,7 @@ func (cli *DockerCli) Apply(ops ...CLIOption) error {
type ServerInfo struct { type ServerInfo struct {
HasExperimental bool HasExperimental bool
OSType string OSType string
BuildkitVersion build.BuilderVersion BuildkitVersion types.BuilderVersion
// SwarmStatus provides information about the current swarm status of the // SwarmStatus provides information about the current swarm status of the
// engine, obtained from the "Swarm" header in the API response. // engine, obtained from the "Swarm" header in the API response.
@ -539,15 +553,18 @@ func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
return cli, nil return cli, nil
} }
func getServerHost(hosts []string, defaultToTLS bool) (string, error) { func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
var host string
switch len(hosts) { switch len(hosts) {
case 0: case 0:
return dopts.ParseHost(defaultToTLS, os.Getenv(client.EnvOverrideHost)) host = os.Getenv(client.EnvOverrideHost)
case 1: case 1:
return dopts.ParseHost(defaultToTLS, hosts[0]) host = hosts[0]
default: default:
return "", errors.New("Specify only one -H") return "", errors.New("Specify only one -H")
} }
return dopts.ParseHost(tlsOptions != nil, host)
} }
// UserAgent returns the user agent string used for making API requests // UserAgent returns the user agent string used for making API requests

View File

@ -10,7 +10,8 @@ import (
"strings" "strings"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/moby/moby/client" "github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/moby/term" "github.com/moby/term"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -100,8 +101,7 @@ func WithContentTrust(enabled bool) CLIOption {
// WithDefaultContextStoreConfig configures the cli to use the default context store configuration. // WithDefaultContextStoreConfig configures the cli to use the default context store configuration.
func WithDefaultContextStoreConfig() CLIOption { func WithDefaultContextStoreConfig() CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cfg := DefaultContextStoreConfig() cli.contextStoreConfig = DefaultContextStoreConfig()
cli.contextStoreConfig = &cfg
return nil return nil
} }
} }
@ -114,18 +114,6 @@ func WithAPIClient(c client.APIClient) CLIOption {
} }
} }
// WithInitializeClient is passed to [DockerCli.Initialize] to initialize
// an API Client for use by the CLI.
func WithInitializeClient(makeClient func(*DockerCli) (client.APIClient, error)) CLIOption {
return func(cli *DockerCli) error {
c, err := makeClient(cli)
if err != nil {
return err
}
return WithAPIClient(c)(cli)
}
}
// envOverrideHTTPHeaders is the name of the environment-variable that can be // 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 // 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 // variable is the equivalent to the HttpHeaders field in the configuration
@ -189,10 +177,7 @@ func withCustomHeadersFromEnv() client.Opt {
csvReader := csv.NewReader(strings.NewReader(value)) csvReader := csv.NewReader(strings.NewReader(value))
fields, err := csvReader.Read() fields, err := csvReader.Read()
if err != nil { if err != nil {
return invalidParameter(errors.Errorf( 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))
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
envOverrideHTTPHeaders,
))
} }
if len(fields) == 0 { if len(fields) == 0 {
return nil return nil
@ -206,10 +191,7 @@ func withCustomHeadersFromEnv() client.Opt {
k = strings.TrimSpace(k) k = strings.TrimSpace(k)
if k == "" { if k == "" {
return invalidParameter(errors.Errorf( 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))
`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. // We don't currently allow empty key=value pairs, and produce an error.
@ -217,10 +199,7 @@ func withCustomHeadersFromEnv() client.Opt {
// from an environment variable with the same name). In the meantime, // from an environment variable with the same name). In the meantime,
// produce an error to prevent users from depending on this. // produce an error to prevent users from depending on this.
if !hasValue { if !hasValue {
return invalidParameter(errors.Errorf( return errdefs.InvalidParameter(errors.Errorf(`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`, envOverrideHTTPHeaders, kv))
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
envOverrideHTTPHeaders, kv,
))
} }
env[http.CanonicalHeaderKey(k)] = v env[http.CanonicalHeaderKey(k)] = v

View File

@ -3,7 +3,6 @@ package command
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -19,9 +18,13 @@ import (
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/flags" "github.com/docker/cli/cli/flags"
"github.com/moby/moby/api/types" "github.com/docker/cli/cli/streams"
"github.com/moby/moby/client" "github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
"gotest.tools/v3/fs"
) )
func TestNewAPIClientFromFlags(t *testing.T) { func TestNewAPIClientFromFlags(t *testing.T) {
@ -33,7 +36,7 @@ func TestNewAPIClientFromFlags(t *testing.T) {
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{}) apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host) assert.Equal(t, apiClient.DaemonHost(), host)
assert.Equal(t, apiClient.ClientVersion(), client.DefaultAPIVersion) assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
} }
func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) { func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
@ -46,7 +49,7 @@ func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{}) apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), slug+host) assert.Equal(t, apiClient.DaemonHost(), slug+host)
assert.Equal(t, apiClient.ClientVersion(), client.DefaultAPIVersion) assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
} }
func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) { func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
@ -70,7 +73,7 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
apiClient, err := NewAPIClientFromFlags(opts, configFile) apiClient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host) assert.Equal(t, apiClient.DaemonHost(), host)
assert.Equal(t, apiClient.ClientVersion(), client.DefaultAPIVersion) assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
// verify User-Agent is not appended to the configfile. see https://github.com/docker/cli/pull/2756 // verify User-Agent is not appended to the configfile. see https://github.com/docker/cli/pull/2756
assert.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"}) assert.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"})
@ -105,7 +108,7 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
apiClient, err := NewAPIClientFromFlags(opts, configFile) apiClient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host) assert.Equal(t, apiClient.DaemonHost(), host)
assert.Equal(t, apiClient.ClientVersion(), client.DefaultAPIVersion) assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
expectedHeaders := http.Header{ expectedHeaders := http.Header{
"One": []string{"one-value"}, "One": []string{"one-value"},
@ -120,8 +123,7 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
} }
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) { func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
const customVersion = "v3.3.3" customVersion := "v3.3.3"
const expectedVersion = "3.3.3"
t.Setenv("DOCKER_API_VERSION", customVersion) t.Setenv("DOCKER_API_VERSION", customVersion)
t.Setenv("DOCKER_HOST", ":2375") t.Setenv("DOCKER_HOST", ":2375")
@ -129,7 +131,7 @@ func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
configFile := &configfile.ConfigFile{} configFile := &configfile.ConfigFile{}
apiclient, err := NewAPIClientFromFlags(opts, configFile) apiclient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, apiclient.ClientVersion(), expectedVersion) assert.Equal(t, apiclient.ClientVersion(), customVersion)
} }
type fakeClient struct { type fakeClient struct {
@ -185,18 +187,19 @@ func TestInitializeFromClient(t *testing.T) {
}, },
} }
for _, tc := range testcases { for _, testcase := range testcases {
t.Run(tc.doc, func(t *testing.T) { testcase := testcase
t.Run(testcase.doc, func(t *testing.T) {
apiclient := &fakeClient{ apiclient := &fakeClient{
pingFunc: tc.pingFunc, pingFunc: testcase.pingFunc,
version: defaultVersion, version: defaultVersion,
} }
cli := &DockerCli{client: apiclient} cli := &DockerCli{client: apiclient}
err := cli.Initialize(flags.NewClientOptions()) err := cli.Initialize(flags.NewClientOptions())
assert.NilError(t, err) assert.NilError(t, err)
assert.DeepEqual(t, cli.ServerInfo(), tc.expectedServer) assert.DeepEqual(t, cli.ServerInfo(), testcase.expectedServer)
assert.Equal(t, apiclient.negotiated, tc.negotiated) assert.Equal(t, apiclient.negotiated, testcase.negotiated)
}) })
} }
} }
@ -204,8 +207,8 @@ func TestInitializeFromClient(t *testing.T) {
// Makes sure we don't hang forever on the initial connection. // Makes sure we don't hang forever on the initial connection.
// https://github.com/docker/cli/issues/3652 // https://github.com/docker/cli/issues/3652
func TestInitializeFromClientHangs(t *testing.T) { func TestInitializeFromClientHangs(t *testing.T) {
tmpDir := t.TempDir() dir := t.TempDir()
socket := filepath.Join(tmpDir, "my.sock") socket := filepath.Join(dir, "my.sock")
l, err := net.Listen("unix", socket) l, err := net.Listen("unix", socket)
assert.NilError(t, err) assert.NilError(t, err)
@ -253,40 +256,80 @@ func TestInitializeFromClientHangs(t *testing.T) {
} }
} }
// The CLI no longer disables/hides experimental CLI features, however, we need
// to verify that existing configuration files do not break
func TestExperimentalCLI(t *testing.T) {
defaultVersion := "v1.55"
testcases := []struct {
doc string
configfile string
}{
{
doc: "default",
configfile: `{}`,
},
{
doc: "experimental",
configfile: `{
"experimental": "enabled"
}`,
},
}
for _, testcase := range testcases {
testcase := testcase
t.Run(testcase.doc, func(t *testing.T) {
dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile))
defer dir.Remove()
apiclient := &fakeClient{
version: defaultVersion,
pingFunc: func() (types.Ping, error) {
return types.Ping{Experimental: true, OSType: "linux", APIVersion: defaultVersion}, nil
},
}
cli := &DockerCli{client: apiclient, err: streams.NewOut(os.Stderr)}
config.SetDir(dir.Path())
err := cli.Initialize(flags.NewClientOptions())
assert.NilError(t, err)
})
}
}
func TestNewDockerCliAndOperators(t *testing.T) { func TestNewDockerCliAndOperators(t *testing.T) {
// Test default operations and also overriding default ones // Test default operations and also overriding default ones
cli, err := NewDockerCli(WithInputStream(io.NopCloser(strings.NewReader("some input")))) cli, err := NewDockerCli(
WithContentTrust(true),
)
assert.NilError(t, err) assert.NilError(t, err)
// Check streams are initialized // Check streams are initialized
assert.Check(t, cli.In() != nil) assert.Check(t, cli.In() != nil)
assert.Check(t, cli.Out() != nil) assert.Check(t, cli.Out() != nil)
assert.Check(t, cli.Err() != nil) assert.Check(t, cli.Err() != nil)
inputStream, err := io.ReadAll(cli.In()) assert.Equal(t, cli.ContentTrustEnabled(), true)
assert.NilError(t, err)
assert.Equal(t, string(inputStream), "some input")
// Apply can modify a dockerCli after construction // Apply can modify a dockerCli after construction
inbuf := bytes.NewBuffer([]byte("input"))
outbuf := bytes.NewBuffer(nil) outbuf := bytes.NewBuffer(nil)
errbuf := bytes.NewBuffer(nil) errbuf := bytes.NewBuffer(nil)
err = cli.Apply( err = cli.Apply(
WithInputStream(io.NopCloser(strings.NewReader("input"))), WithInputStream(io.NopCloser(inbuf)),
WithOutputStream(outbuf), WithOutputStream(outbuf),
WithErrorStream(errbuf), WithErrorStream(errbuf),
) )
assert.NilError(t, err) assert.NilError(t, err)
// Check input stream // Check input stream
inputStream, err = io.ReadAll(cli.In()) inputStream, err := io.ReadAll(cli.In())
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, string(inputStream), "input") assert.Equal(t, string(inputStream), "input")
// Check output stream // Check output stream
_, err = fmt.Fprint(cli.Out(), "output") fmt.Fprintf(cli.Out(), "output")
assert.NilError(t, err)
outputStream, err := io.ReadAll(outbuf) outputStream, err := io.ReadAll(outbuf)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, string(outputStream), "output") assert.Equal(t, string(outputStream), "output")
// Check error stream // Check error stream
_, err = fmt.Fprint(cli.Err(), "error") fmt.Fprintf(cli.Err(), "error")
assert.NilError(t, err)
errStream, err := io.ReadAll(errbuf) errStream, err := io.ReadAll(errbuf)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, string(errStream), "error") assert.Equal(t, string(errStream), "error")
@ -303,8 +346,6 @@ func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
func TestHooksEnabled(t *testing.T) { func TestHooksEnabled(t *testing.T) {
t.Run("disabled by default", func(t *testing.T) { t.Run("disabled by default", func(t *testing.T) {
// Make sure we don't depend on any existing ~/.docker/config.json
config.SetDir(t.TempDir())
cli, err := NewDockerCli() cli, err := NewDockerCli()
assert.NilError(t, err) assert.NilError(t, err)
@ -316,11 +357,12 @@ func TestHooksEnabled(t *testing.T) {
"features": { "features": {
"hooks": "true" "hooks": "true"
}}` }}`
config.SetDir(t.TempDir()) dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600) defer dir.Remove()
assert.NilError(t, err)
cli, err := NewDockerCli() cli, err := NewDockerCli()
assert.NilError(t, err) assert.NilError(t, err)
config.SetDir(dir.Path())
assert.Check(t, cli.HooksEnabled()) assert.Check(t, cli.HooksEnabled())
}) })
@ -330,11 +372,12 @@ func TestHooksEnabled(t *testing.T) {
"hooks": "true" "hooks": "true"
}}` }}`
t.Setenv("DOCKER_CLI_HOOKS", "false") t.Setenv("DOCKER_CLI_HOOKS", "false")
config.SetDir(t.TempDir()) dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600) defer dir.Remove()
assert.NilError(t, err)
cli, err := NewDockerCli() cli, err := NewDockerCli()
assert.NilError(t, err) assert.NilError(t, err)
config.SetDir(dir.Path())
assert.Check(t, !cli.HooksEnabled()) assert.Check(t, !cli.HooksEnabled())
}) })
@ -344,11 +387,12 @@ func TestHooksEnabled(t *testing.T) {
"hooks": "true" "hooks": "true"
}}` }}`
t.Setenv("DOCKER_CLI_HINTS", "false") t.Setenv("DOCKER_CLI_HINTS", "false")
config.SetDir(t.TempDir()) dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600) defer dir.Remove()
assert.NilError(t, err)
cli, err := NewDockerCli() cli, err := NewDockerCli()
assert.NilError(t, err) assert.NilError(t, err)
config.SetDir(dir.Path())
assert.Check(t, !cli.HooksEnabled()) assert.Check(t, !cli.HooksEnabled())
}) })
} }

View File

@ -43,7 +43,6 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
system.NewInfoCommand(dockerCli), system.NewInfoCommand(dockerCli),
// management commands // management commands
builder.NewBakeStubCommand(dockerCli),
builder.NewBuilderCommand(dockerCli), builder.NewBuilderCommand(dockerCli),
checkpoint.NewCheckpointCommand(dockerCli), checkpoint.NewCheckpointCommand(dockerCli),
container.NewContainerCommand(dockerCli), container.NewContainerCommand(dockerCli),

View File

@ -5,18 +5,17 @@ import (
"strings" "strings"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/container" "github.com/docker/docker/api/types"
"github.com/moby/moby/api/types/image" "github.com/docker/docker/api/types/container"
"github.com/moby/moby/api/types/network" "github.com/docker/docker/api/types/image"
"github.com/moby/moby/api/types/volume" "github.com/docker/docker/api/types/network"
"github.com/moby/moby/client" "github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// ValidArgsFn a function to be used by cobra command as `ValidArgsFunction` to offer command line completion. // ValidArgsFn a function to be used by cobra command as `ValidArgsFunction` to offer command line completion
// type ValidArgsFn func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
// Deprecated: use [cobra.CompletionFunc].
type ValidArgsFn = cobra.CompletionFunc
// APIClientProvider provides a method to get an [client.APIClient], initializing // APIClientProvider provides a method to get an [client.APIClient], initializing
// it if needed. // it if needed.
@ -29,11 +28,8 @@ type APIClientProvider interface {
} }
// ImageNames offers completion for images present within the local store // ImageNames offers completion for images present within the local store
func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc { func ImageNames(dockerCLI APIClientProvider) ValidArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if limit > 0 && len(args) >= limit {
return nil, cobra.ShellCompDirectiveNoFileComp
}
list, err := dockerCLI.Client().ImageList(cmd.Context(), image.ListOptions{}) list, err := dockerCLI.Client().ImageList(cmd.Context(), image.ListOptions{})
if err != nil { if err != nil {
return nil, cobra.ShellCompDirectiveError return nil, cobra.ShellCompDirectiveError
@ -49,7 +45,7 @@ func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
// ContainerNames offers completion for container names and IDs // ContainerNames offers completion for container names and IDs
// By default, only names are returned. // By default, only names are returned.
// Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs. // Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs.
func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(container.Summary) bool) cobra.CompletionFunc { func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(types.Container) bool) ValidArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, err := dockerCLI.Client().ContainerList(cmd.Context(), container.ListOptions{ list, err := dockerCLI.Client().ContainerList(cmd.Context(), container.ListOptions{
All: all, All: all,
@ -64,7 +60,7 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
for _, ctr := range list { for _, ctr := range list {
skip := false skip := false
for _, fn := range filters { for _, fn := range filters {
if fn != nil && !fn(ctr) { if !fn(ctr) {
skip = true skip = true
break break
} }
@ -82,7 +78,7 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
} }
// VolumeNames offers completion for volumes // VolumeNames offers completion for volumes
func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc { func VolumeNames(dockerCLI APIClientProvider) ValidArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{}) list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{})
if err != nil { if err != nil {
@ -97,7 +93,7 @@ func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
} }
// NetworkNames offers completion for networks // NetworkNames offers completion for networks
func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc { func NetworkNames(dockerCLI APIClientProvider) ValidArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{}) list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{})
if err != nil { if err != nil {
@ -135,7 +131,7 @@ func EnvVarNames(_ *cobra.Command, _ []string, _ string) (names []string, _ cobr
} }
// FromList offers completion for the given list of options. // FromList offers completion for the given list of options.
func FromList(options ...string) cobra.CompletionFunc { func FromList(options ...string) ValidArgsFn {
return cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp) return cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp)
} }
@ -150,44 +146,3 @@ func FileNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCom
func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp
} }
var commonPlatforms = []string{
"linux/386",
"linux/amd64",
"linux/arm",
"linux/arm/v5",
"linux/arm/v6",
"linux/arm/v7",
"linux/arm64",
"linux/arm64/v8",
// IBM power and z platforms
"linux/ppc64le",
"linux/s390x",
// Not yet supported
"linux/riscv64",
"windows/amd64",
"wasip1/wasm",
}
// Platforms offers completion for platform-strings. It provides a non-exhaustive
// list of platforms to be used for completion. Platform-strings are based on
// [runtime.GOOS] and [runtime.GOARCH], but with (optional) variants added. A
// list of recognised os/arch combinations from the Go runtime can be obtained
// through "go tool dist list".
//
// Some noteworthy exclusions from this list:
//
// - arm64 images ("windows/arm64", "windows/arm64/v8") do not yet exist for windows.
// - we don't (yet) include `os-variant` for completion (as can be used for Windows images)
// - we don't (yet) include platforms for which we don't build binaries, such as
// BSD platforms (freebsd, netbsd, openbsd), android, macOS (darwin).
// - we currently exclude architectures that may have unofficial builds,
// but don't have wide adoption (and no support), such as loong64, mipsXXX,
// ppc64 (non-le) to prevent confusion.
func Platforms(_ *cobra.Command, _ []string, _ string) (platforms []string, _ cobra.ShellCompDirective) {
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
}

View File

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

View File

@ -3,23 +3,24 @@ package config
import ( import (
"context" "context"
"github.com/moby/moby/api/types/swarm" "github.com/docker/docker/api/types"
"github.com/moby/moby/client" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
) )
type fakeClient struct { type fakeClient struct {
client.Client client.Client
configCreateFunc func(context.Context, swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) configCreateFunc func(context.Context, swarm.ConfigSpec) (types.ConfigCreateResponse, error)
configInspectFunc func(context.Context, string) (swarm.Config, []byte, error) configInspectFunc func(context.Context, string) (swarm.Config, []byte, error)
configListFunc func(context.Context, swarm.ConfigListOptions) ([]swarm.Config, error) configListFunc func(context.Context, types.ConfigListOptions) ([]swarm.Config, error)
configRemoveFunc func(string) error configRemoveFunc func(string) error
} }
func (c *fakeClient) ConfigCreate(ctx context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) { func (c *fakeClient) ConfigCreate(ctx context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if c.configCreateFunc != nil { if c.configCreateFunc != nil {
return c.configCreateFunc(ctx, spec) return c.configCreateFunc(ctx, spec)
} }
return swarm.ConfigCreateResponse{}, nil return types.ConfigCreateResponse{}, nil
} }
func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) { func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
@ -29,7 +30,7 @@ func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm
return swarm.Config{}, nil, nil return swarm.Config{}, nil, nil
} }
func (c *fakeClient) ConfigList(ctx context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { func (c *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
if c.configListFunc != nil { if c.configListFunc != nil {
return c.configListFunc(ctx, options) return c.configListFunc(ctx, options)
} }

View File

@ -4,7 +4,7 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/moby/moby/api/types/swarm" "github.com/docker/docker/api/types"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -30,9 +30,9 @@ func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
} }
// completeNames offers completion for swarm configs // completeNames offers completion for swarm configs
func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc { func completeNames(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, err := dockerCLI.Client().ConfigList(cmd.Context(), swarm.ConfigListOptions{}) list, err := dockerCLI.Client().ConfigList(cmd.Context(), types.ConfigListOptions{})
if err != nil { if err != nil {
return nil, cobra.ShellCompDirectiveError return nil, cobra.ShellCompDirectiveError
} }

View File

@ -9,7 +9,7 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/moby/moby/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/moby/sys/sequential" "github.com/moby/sys/sequential"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -48,10 +48,20 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
} }
// RunConfigCreate creates a config with the given options. // RunConfigCreate creates a config with the given options.
func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateOptions) error { func RunConfigCreate(ctx context.Context, dockerCli command.Cli, options CreateOptions) error {
apiClient := dockerCLI.Client() client := dockerCli.Client()
configData, err := readConfigData(dockerCLI.In(), options.File) var in io.Reader = dockerCli.In()
if options.File != "-" {
file, err := sequential.Open(options.File)
if err != nil {
return err
}
in = file
defer file.Close()
}
configData, err := io.ReadAll(in)
if err != nil { if err != nil {
return errors.Errorf("Error reading content from %q: %v", options.File, err) return errors.Errorf("Error reading content from %q: %v", options.File, err)
} }
@ -59,7 +69,7 @@ func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateO
spec := swarm.ConfigSpec{ spec := swarm.ConfigSpec{
Annotations: swarm.Annotations{ Annotations: swarm.Annotations{
Name: options.Name, Name: options.Name,
Labels: opts.ConvertKVStringsToMap(options.Labels.GetSlice()), Labels: opts.ConvertKVStringsToMap(options.Labels.GetAll()),
}, },
Data: configData, Data: configData,
} }
@ -68,59 +78,11 @@ func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateO
Name: options.TemplateDriver, Name: options.TemplateDriver,
} }
} }
r, err := apiClient.ConfigCreate(ctx, spec) r, err := client.ConfigCreate(ctx, spec)
if err != nil { if err != nil {
return err return err
} }
_, _ = fmt.Fprintln(dockerCLI.Out(), r.ID) fmt.Fprintln(dockerCli.Out(), r.ID)
return nil return nil
} }
// maxConfigSize is the maximum byte length of the [swarm.ConfigSpec.Data] field,
// as defined by [MaxConfigSize] in SwarmKit.
//
// [MaxConfigSize]: https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/manager/controlapi#MaxConfigSize
const maxConfigSize = 1000 * 1024 // 1000KB
// readConfigData reads the config from either stdin or the given fileName.
//
// It reads up to twice the maximum size of the config ([maxConfigSize]),
// just in case swarm's limit changes; this is only a safeguard to prevent
// reading arbitrary files into memory.
func readConfigData(in io.Reader, fileName string) ([]byte, error) {
switch fileName {
case "-":
data, err := io.ReadAll(io.LimitReader(in, 2*maxConfigSize))
if err != nil {
return nil, fmt.Errorf("error reading from STDIN: %w", err)
}
if len(data) == 0 {
return nil, errors.New("error reading from STDIN: data is empty")
}
return data, nil
case "":
return nil, errors.New("config file is required")
default:
// Open file with [FILE_FLAG_SEQUENTIAL_SCAN] on Windows, which
// prevents Windows from aggressively caching it. We expect this
// file to be only read once. Given that this is expected to be
// a small file, this may not be a significant optimization, so
// we could choose to omit this, and use a regular [os.Open].
//
// [FILE_FLAG_SEQUENTIAL_SCAN]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
f, err := sequential.Open(fileName)
if err != nil {
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
}
defer f.Close()
data, err := io.ReadAll(io.LimitReader(f, 2*maxConfigSize))
if err != nil {
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
}
if len(data) == 0 {
return nil, fmt.Errorf("error reading from %s: data is empty", fileName)
}
return data, nil
}
}

View File

@ -2,8 +2,6 @@ package config
import ( import (
"context" "context"
"errors"
"fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@ -12,7 +10,9 @@ import (
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/swarm" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
@ -23,26 +23,27 @@ const configDataFile = "config-create-with-name.golden"
func TestConfigCreateErrors(t *testing.T) { func TestConfigCreateErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
configCreateFunc func(context.Context, swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) configCreateFunc func(context.Context, swarm.ConfigSpec) (types.ConfigCreateResponse, error)
expectedError string expectedError string
}{ }{
{ {
args: []string{"too_few"}, args: []string{"too_few"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {
args: []string{"too", "many", "arguments"}, args: []string{"too", "many", "arguments"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {
args: []string{"name", filepath.Join("testdata", configDataFile)}, args: []string{"name", filepath.Join("testdata", configDataFile)},
configCreateFunc: func(_ context.Context, configSpec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) { configCreateFunc: func(_ context.Context, configSpec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
return swarm.ConfigCreateResponse{}, errors.New("error creating config") return types.ConfigCreateResponse{}, errors.Errorf("error creating config")
}, },
expectedError: "error creating config", expectedError: "error creating config",
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.expectedError, func(t *testing.T) { t.Run(tc.expectedError, func(t *testing.T) {
cmd := newConfigCreateCommand( cmd := newConfigCreateCommand(
test.NewFakeCli(&fakeClient{ test.NewFakeCli(&fakeClient{
@ -58,17 +59,17 @@ func TestConfigCreateErrors(t *testing.T) {
} }
func TestConfigCreateWithName(t *testing.T) { func TestConfigCreateWithName(t *testing.T) {
const name = "config-with-name" name := "foo"
var actual []byte var actual []byte
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) { configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if spec.Name != name { if spec.Name != name {
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name) return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
} }
actual = spec.Data actual = spec.Data
return swarm.ConfigCreateResponse{ return types.ConfigCreateResponse{
ID: "ID-" + spec.Name, ID: "ID-" + spec.Name,
}, nil }, nil
}, },
@ -86,7 +87,7 @@ func TestConfigCreateWithLabels(t *testing.T) {
"lbl1": "Label-foo", "lbl1": "Label-foo",
"lbl2": "Label-bar", "lbl2": "Label-bar",
} }
const name = "config-with-labels" name := "foo"
data, err := os.ReadFile(filepath.Join("testdata", configDataFile)) data, err := os.ReadFile(filepath.Join("testdata", configDataFile))
assert.NilError(t, err) assert.NilError(t, err)
@ -100,12 +101,12 @@ func TestConfigCreateWithLabels(t *testing.T) {
} }
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) { configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if !reflect.DeepEqual(spec, expected) { if !reflect.DeepEqual(spec, expected) {
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected %+v, got %+v", expected, spec) return types.ConfigCreateResponse{}, errors.Errorf("expected %+v, got %+v", expected, spec)
} }
return swarm.ConfigCreateResponse{ return types.ConfigCreateResponse{
ID: "ID-" + spec.Name, ID: "ID-" + spec.Name,
}, nil }, nil
}, },
@ -123,19 +124,19 @@ func TestConfigCreateWithTemplatingDriver(t *testing.T) {
expectedDriver := &swarm.Driver{ expectedDriver := &swarm.Driver{
Name: "template-driver", Name: "template-driver",
} }
const name = "config-with-template-driver" name := "foo"
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) { configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if spec.Name != name { if spec.Name != name {
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name) return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
} }
if spec.Templating.Name != expectedDriver.Name { if spec.Templating.Name != expectedDriver.Name {
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels) return types.ConfigCreateResponse{}, errors.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
} }
return swarm.ConfigCreateResponse{ return types.ConfigCreateResponse{
ID: "ID-" + spec.Name, ID: "ID-" + spec.Name,
}, nil }, nil
}, },

View File

@ -5,10 +5,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/inspect" "github.com/docker/cli/cli/command/inspect"
"github.com/docker/go-units" "github.com/docker/docker/api/types/swarm"
"github.com/moby/moby/api/types/swarm" units "github.com/docker/go-units"
) )
const ( const (
@ -156,11 +157,11 @@ func (ctx *configInspectContext) Labels() map[string]string {
} }
func (ctx *configInspectContext) CreatedAt() string { func (ctx *configInspectContext) CreatedAt() string {
return formatter.PrettyPrint(ctx.Config.CreatedAt) return command.PrettyPrint(ctx.Config.CreatedAt)
} }
func (ctx *configInspectContext) UpdatedAt() string { func (ctx *configInspectContext) UpdatedAt() string {
return formatter.PrettyPrint(ctx.Config.UpdatedAt) return command.PrettyPrint(ctx.Config.UpdatedAt)
} }
func (ctx *configInspectContext) Data() string { func (ctx *configInspectContext) Data() string {

View File

@ -6,7 +6,7 @@ import (
"time" "time"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
) )
@ -61,6 +61,7 @@ id_rsa
}, },
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out

View File

@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.23 //go:build go1.21
package config package config
@ -43,15 +43,15 @@ func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command {
} }
// RunConfigInspect inspects the given Swarm config. // RunConfigInspect inspects the given Swarm config.
func RunConfigInspect(ctx context.Context, dockerCLI command.Cli, opts InspectOptions) error { func RunConfigInspect(ctx context.Context, dockerCli command.Cli, opts InspectOptions) error {
apiClient := dockerCLI.Client() client := dockerCli.Client()
if opts.Pretty { if opts.Pretty {
opts.Format = "pretty" opts.Format = "pretty"
} }
getRef := func(id string) (any, []byte, error) { getRef := func(id string) (any, []byte, error) {
return apiClient.ConfigInspectWithRaw(ctx, id) return client.ConfigInspectWithRaw(ctx, id)
} }
f := opts.Format f := opts.Format
@ -62,7 +62,7 @@ func RunConfigInspect(ctx context.Context, dockerCLI command.Cli, opts InspectOp
} }
configCtx := formatter.Context{ configCtx := formatter.Context{
Output: dockerCLI.Out(), Output: dockerCli.Out(),
Format: NewFormat(f, false), Format: NewFormat(f, false),
} }

View File

@ -2,7 +2,6 @@ package config
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"testing" "testing"
@ -10,7 +9,8 @@ import (
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/builders" "github.com/docker/cli/internal/test/builders"
"github.com/moby/moby/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
) )
@ -28,7 +28,7 @@ func TestConfigInspectErrors(t *testing.T) {
{ {
args: []string{"foo"}, args: []string{"foo"},
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) { configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
return swarm.Config{}, nil, errors.New("error while inspecting the config") return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
}, },
expectedError: "error while inspecting the config", expectedError: "error while inspecting the config",
}, },
@ -45,7 +45,7 @@ func TestConfigInspectErrors(t *testing.T) {
if configID == "foo" { if configID == "foo" {
return *builders.Config(builders.ConfigName("foo")), nil, nil return *builders.Config(builders.ConfigName("foo")), nil, nil
} }
return swarm.Config{}, nil, errors.New("error while inspecting the config") return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
}, },
expectedError: "error while inspecting the config", expectedError: "error while inspecting the config",
}, },
@ -77,7 +77,7 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
args: []string{"foo"}, args: []string{"foo"},
configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) { configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
if name != "foo" { if name != "foo" {
return swarm.Config{}, nil, fmt.Errorf("invalid name, expected %s, got %s", "foo", name) return swarm.Config{}, nil, errors.Errorf("Invalid name, expected %s, got %s", "foo", name)
} }
return *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), nil, nil return *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), nil, nil
}, },

View File

@ -10,8 +10,8 @@ import (
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
flagsHelper "github.com/docker/cli/cli/flags" flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
"github.com/moby/moby/api/types/swarm"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -45,18 +45,18 @@ func newConfigListCommand(dockerCli command.Cli) *cobra.Command {
} }
// RunConfigList lists Swarm configs. // RunConfigList lists Swarm configs.
func RunConfigList(ctx context.Context, dockerCLI command.Cli, options ListOptions) error { func RunConfigList(ctx context.Context, dockerCli command.Cli, options ListOptions) error {
apiClient := dockerCLI.Client() client := dockerCli.Client()
configs, err := apiClient.ConfigList(ctx, swarm.ConfigListOptions{Filters: options.Filter.Value()}) configs, err := client.ConfigList(ctx, types.ConfigListOptions{Filters: options.Filter.Value()})
if err != nil { if err != nil {
return err return err
} }
format := options.Format format := options.Format
if len(format) == 0 { if len(format) == 0 {
if len(dockerCLI.ConfigFile().ConfigFormat) > 0 && !options.Quiet { if len(dockerCli.ConfigFile().ConfigFormat) > 0 && !options.Quiet {
format = dockerCLI.ConfigFile().ConfigFormat format = dockerCli.ConfigFile().ConfigFormat
} else { } else {
format = formatter.TableFormatKey format = formatter.TableFormatKey
} }
@ -67,7 +67,7 @@ func RunConfigList(ctx context.Context, dockerCLI command.Cli, options ListOptio
}) })
configCtx := formatter.Context{ configCtx := formatter.Context{
Output: dockerCLI.Out(), Output: dockerCli.Out(),
Format: NewFormat(format, options.Quiet), Format: NewFormat(format, options.Quiet),
} }
return FormatWrite(configCtx, configs) return FormatWrite(configCtx, configs)

View File

@ -2,7 +2,6 @@ package config
import ( import (
"context" "context"
"errors"
"io" "io"
"testing" "testing"
"time" "time"
@ -10,7 +9,9 @@ import (
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/builders" "github.com/docker/cli/internal/test/builders"
"github.com/moby/moby/api/types/swarm" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
@ -19,7 +20,7 @@ import (
func TestConfigListErrors(t *testing.T) { func TestConfigListErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
configListFunc func(context.Context, swarm.ConfigListOptions) ([]swarm.Config, error) configListFunc func(context.Context, types.ConfigListOptions) ([]swarm.Config, error)
expectedError string expectedError string
}{ }{
{ {
@ -27,8 +28,8 @@ func TestConfigListErrors(t *testing.T) {
expectedError: "accepts no argument", expectedError: "accepts no argument",
}, },
{ {
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{}, errors.New("error listing configs") return []swarm.Config{}, errors.Errorf("error listing configs")
}, },
expectedError: "error listing configs", expectedError: "error listing configs",
}, },
@ -48,7 +49,7 @@ func TestConfigListErrors(t *testing.T) {
func TestConfigList(t *testing.T) { func TestConfigList(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{ return []swarm.Config{
*builders.Config(builders.ConfigID("ID-1-foo"), *builders.Config(builders.ConfigID("ID-1-foo"),
builders.ConfigName("1-foo"), builders.ConfigName("1-foo"),
@ -78,7 +79,7 @@ func TestConfigList(t *testing.T) {
func TestConfigListWithQuietOption(t *testing.T) { func TestConfigListWithQuietOption(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{ return []swarm.Config{
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{ *builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
@ -95,7 +96,7 @@ func TestConfigListWithQuietOption(t *testing.T) {
func TestConfigListWithConfigFormat(t *testing.T) { func TestConfigListWithConfigFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{ return []swarm.Config{
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{ *builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
@ -114,7 +115,7 @@ func TestConfigListWithConfigFormat(t *testing.T) {
func TestConfigListWithFormat(t *testing.T) { func TestConfigListWithFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{ return []swarm.Config{
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{ *builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
@ -131,7 +132,7 @@ func TestConfigListWithFormat(t *testing.T) {
func TestConfigListWithFilter(t *testing.T) { func TestConfigListWithFilter(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
assert.Check(t, is.Equal("foo", options.Filters.Get("name")[0])) assert.Check(t, is.Equal("foo", options.Filters.Get("name")[0]))
assert.Check(t, is.Equal("lbl1=Label-bar", options.Filters.Get("label")[0])) assert.Check(t, is.Equal("lbl1=Label-bar", options.Filters.Get("label")[0]))
return []swarm.Config{ return []swarm.Config{

View File

@ -2,11 +2,12 @@ package config
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -34,17 +35,23 @@ func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command {
} }
// RunConfigRemove removes the given Swarm configs. // RunConfigRemove removes the given Swarm configs.
func RunConfigRemove(ctx context.Context, dockerCLI command.Cli, opts RemoveOptions) error { func RunConfigRemove(ctx context.Context, dockerCli command.Cli, opts RemoveOptions) error {
apiClient := dockerCLI.Client() client := dockerCli.Client()
var errs []string
var errs []error
for _, name := range opts.Names { for _, name := range opts.Names {
if err := apiClient.ConfigRemove(ctx, name); err != nil { if err := client.ConfigRemove(ctx, name); err != nil {
errs = append(errs, err) errs = append(errs, err.Error())
continue continue
} }
_, _ = fmt.Fprintln(dockerCLI.Out(), name)
fmt.Fprintln(dockerCli.Out(), name)
} }
return errors.Join(errs...) if len(errs) > 0 {
return errors.Errorf("%s", strings.Join(errs, "\n"))
}
return nil
} }

View File

@ -1,12 +1,12 @@
package config package config
import ( import (
"errors"
"io" "io"
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
) )
@ -19,12 +19,12 @@ func TestConfigRemoveErrors(t *testing.T) {
}{ }{
{ {
args: []string{}, args: []string{},
expectedError: "requires at least 1 argument", expectedError: "requires at least 1 argument.",
}, },
{ {
args: []string{"foo"}, args: []string{"foo"},
configRemoveFunc: func(name string) error { configRemoveFunc: func(name string) error {
return errors.New("error removing config") return errors.Errorf("error removing config")
}, },
expectedError: "error removing config", expectedError: "error removing config",
}, },
@ -66,7 +66,7 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
configRemoveFunc: func(name string) error { configRemoveFunc: func(name string) error {
removedConfigs = append(removedConfigs, name) removedConfigs = append(removedConfigs, name)
if name == "foo" { if name == "foo" {
return errors.New("error removing config: " + name) return errors.Errorf("error removing config: %s", name)
} }
return nil return nil
}, },

View File

@ -7,8 +7,9 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/moby/moby/api/types/container" "github.com/docker/docker/api/types"
"github.com/moby/moby/client" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/moby/sys/signal" "github.com/moby/sys/signal"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -22,7 +23,7 @@ type AttachOptions struct {
DetachKeys string DetachKeys string
} }
func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClient, args string) (*container.InspectResponse, error) { func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClient, args string) (*types.ContainerJSON, error) {
c, err := apiClient.ContainerInspect(ctx, args) c, err := apiClient.ContainerInspect(ctx, args)
if err != nil { if err != nil {
return nil, err return nil, err
@ -55,8 +56,8 @@ func NewAttachCommand(dockerCLI command.Cli) *cobra.Command {
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container attach, docker attach", "aliases": "docker container attach, docker attach",
}, },
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool { ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr types.Container) bool {
return ctr.State != container.StatePaused return ctr.State != "paused"
}), }),
} }
@ -164,6 +165,9 @@ func getExitStatus(errC <-chan error, resultC <-chan container.WaitResponse) err
return cli.StatusError{StatusCode: int(result.StatusCode)} return cli.StatusError{StatusCode: int(result.StatusCode)}
} }
case err := <-errC: case err := <-errC:
if errors.Is(err, context.Canceled) {
return nil
}
return err return err
} }

View File

@ -1,13 +1,15 @@
package container package container
import ( import (
"errors" "context"
"io" "io"
"testing" "testing"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/container" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
) )
@ -16,63 +18,59 @@ func TestNewAttachCommandErrors(t *testing.T) {
name string name string
args []string args []string
expectedError string expectedError string
containerInspectFunc func(img string) (container.InspectResponse, error) containerInspectFunc func(img string) (types.ContainerJSON, error)
}{ }{
{ {
name: "client-error", name: "client-error",
args: []string{"5cb5bb5e4a3b"}, args: []string{"5cb5bb5e4a3b"},
expectedError: "something went wrong", expectedError: "something went wrong",
containerInspectFunc: func(containerID string) (container.InspectResponse, error) { containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
return container.InspectResponse{}, errors.New("something went wrong") return types.ContainerJSON{}, errors.Errorf("something went wrong")
}, },
}, },
{ {
name: "client-stopped", name: "client-stopped",
args: []string{"5cb5bb5e4a3b"}, args: []string{"5cb5bb5e4a3b"},
expectedError: "You cannot attach to a stopped container", expectedError: "You cannot attach to a stopped container",
containerInspectFunc: func(containerID string) (container.InspectResponse, error) { containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
return container.InspectResponse{ c := types.ContainerJSON{}
ContainerJSONBase: &container.ContainerJSONBase{ c.ContainerJSONBase = &types.ContainerJSONBase{}
State: &container.State{ c.ContainerJSONBase.State = &types.ContainerState{Running: false}
Running: false, return c, nil
},
},
}, nil
}, },
}, },
{ {
name: "client-paused", name: "client-paused",
args: []string{"5cb5bb5e4a3b"}, args: []string{"5cb5bb5e4a3b"},
expectedError: "You cannot attach to a paused container", expectedError: "You cannot attach to a paused container",
containerInspectFunc: func(containerID string) (container.InspectResponse, error) { containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
return container.InspectResponse{ c := types.ContainerJSON{}
ContainerJSONBase: &container.ContainerJSONBase{ c.ContainerJSONBase = &types.ContainerJSONBase{}
State: &container.State{ c.ContainerJSONBase.State = &types.ContainerState{
Running: true, Running: true,
Paused: true, Paused: true,
}, }
}, return c, nil
}, nil
}, },
}, },
{ {
name: "client-restarting", name: "client-restarting",
args: []string{"5cb5bb5e4a3b"}, args: []string{"5cb5bb5e4a3b"},
expectedError: "You cannot attach to a restarting container", expectedError: "You cannot attach to a restarting container",
containerInspectFunc: func(containerID string) (container.InspectResponse, error) { containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
return container.InspectResponse{ c := types.ContainerJSON{}
ContainerJSONBase: &container.ContainerJSONBase{ c.ContainerJSONBase = &types.ContainerJSONBase{}
State: &container.State{ c.ContainerJSONBase.State = &types.ContainerState{
Running: true, Running: true,
Paused: false, Paused: false,
Restarting: true, Restarting: true,
}, }
}, return c, nil
}, nil
}, },
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})) cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
@ -112,6 +110,10 @@ func TestGetExitStatus(t *testing.T) {
}, },
expectedError: cli.StatusError{StatusCode: 15}, expectedError: cli.StatusError{StatusCode: 15},
}, },
{
err: context.Canceled,
expectedError: nil,
},
} }
for _, testcase := range testcases { for _, testcase := range testcases {

View File

@ -1,46 +0,0 @@
package container
import (
"fmt"
"os"
"strings"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/types"
)
// readCredentials resolves auth-config from the current environment to be
// applied to the container if the `--use-api-socket` flag is set.
//
// - If a valid "DOCKER_AUTH_CONFIG" env-var is found, and it contains
// credentials, it's value is used.
// - If no "DOCKER_AUTH_CONFIG" env-var is found, or it does not contain
// credentials, it attempts to read from the CLI's credentials store.
//
// It returns an error if either the "DOCKER_AUTH_CONFIG" is incorrectly
// formatted, or when failing to read from the credentials store.
//
// A nil value is returned if neither option contained any credentials.
func readCredentials(dockerCLI config.Provider) (creds map[string]types.AuthConfig, _ error) {
if v, ok := os.LookupEnv("DOCKER_AUTH_CONFIG"); ok && v != "" {
// The results are expected to have been unmarshaled the same as
// when reading from a config-file, which includes decoding the
// base64-encoded "username:password" into the "UserName" and
// "Password" fields.
ac := &configfile.ConfigFile{}
if err := ac.LoadFromReader(strings.NewReader(v)); err != nil {
return nil, fmt.Errorf("failed to read credentials from DOCKER_AUTH_CONFIG: %w", err)
}
if len(ac.AuthConfigs) > 0 {
return ac.AuthConfigs, nil
}
}
// Resolve this here for later, ensuring we error our before we create the container.
creds, err := dockerCLI.ConfigFile().GetAllCredentials()
if err != nil {
return nil, fmt.Errorf("resolving credentials failed: %w", err)
}
return creds, nil
}

View File

@ -4,68 +4,62 @@ import (
"context" "context"
"io" "io"
"github.com/moby/moby/api/types" "github.com/docker/docker/api/types"
"github.com/moby/moby/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/moby/moby/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/moby/moby/api/types/image" "github.com/docker/docker/api/types/image"
"github.com/moby/moby/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/moby/moby/api/types/system" "github.com/docker/docker/api/types/system"
"github.com/moby/moby/client" "github.com/docker/docker/client"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/image-spec/specs-go/v1"
) )
type fakeClient struct { type fakeClient struct {
client.Client client.Client
inspectFunc func(string) (container.InspectResponse, error) inspectFunc func(string) (types.ContainerJSON, error)
execInspectFunc func(execID string) (container.ExecInspect, error) execInspectFunc func(execID string) (container.ExecInspect, error)
execCreateFunc func(containerID string, options container.ExecOptions) (container.ExecCreateResponse, error) execCreateFunc func(containerID string, options container.ExecOptions) (types.IDResponse, error)
createContainerFunc func(config *container.Config, createContainerFunc func(config *container.Config,
hostConfig *container.HostConfig, hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig, networkingConfig *network.NetworkingConfig,
platform *ocispec.Platform, platform *specs.Platform,
containerName string) (container.CreateResponse, error) containerName string) (container.CreateResponse, error)
containerStartFunc func(containerID string, options container.StartOptions) error containerStartFunc func(containerID string, options container.StartOptions) error
imageCreateFunc func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) imageCreateFunc func(parentReference string, options image.CreateOptions) (io.ReadCloser, error)
infoFunc func() (system.Info, error) infoFunc func() (system.Info, error)
containerStatPathFunc func(containerID, path string) (container.PathStat, error) containerStatPathFunc func(containerID, path string) (container.PathStat, error)
containerCopyFromFunc func(containerID, srcPath string) (io.ReadCloser, container.PathStat, error) containerCopyFromFunc func(containerID, srcPath string) (io.ReadCloser, container.PathStat, error)
logFunc func(string, container.LogsOptions) (io.ReadCloser, error) logFunc func(string, container.LogsOptions) (io.ReadCloser, error)
waitFunc func(string) (<-chan container.WaitResponse, <-chan error) waitFunc func(string) (<-chan container.WaitResponse, <-chan error)
containerListFunc func(container.ListOptions) ([]container.Summary, error) containerListFunc func(container.ListOptions) ([]types.Container, error)
containerExportFunc func(string) (io.ReadCloser, error) containerExportFunc func(string) (io.ReadCloser, error)
containerExecResizeFunc func(id string, options container.ResizeOptions) error containerExecResizeFunc func(id string, options container.ResizeOptions) error
containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error
containerRestartFunc func(ctx context.Context, containerID string, options container.StopOptions) error
containerStopFunc func(ctx context.Context, containerID string, options container.StopOptions) error
containerKillFunc func(ctx context.Context, containerID, signal string) error containerKillFunc func(ctx context.Context, containerID, signal string) error
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error)
containerAttachFunc func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) containerAttachFunc func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error)
containerDiffFunc func(ctx context.Context, containerID string) ([]container.FilesystemChange, error)
containerRenameFunc func(ctx context.Context, oldName, newName string) error
containerCommitFunc func(ctx context.Context, container string, options container.CommitOptions) (container.CommitResponse, error)
containerPauseFunc func(ctx context.Context, container string) error
Version string Version string
} }
func (f *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]container.Summary, error) { func (f *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]types.Container, error) {
if f.containerListFunc != nil { if f.containerListFunc != nil {
return f.containerListFunc(options) return f.containerListFunc(options)
} }
return []container.Summary{}, nil return []types.Container{}, nil
} }
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (container.InspectResponse, error) { func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
if f.inspectFunc != nil { if f.inspectFunc != nil {
return f.inspectFunc(containerID) return f.inspectFunc(containerID)
} }
return container.InspectResponse{}, nil return types.ContainerJSON{}, nil
} }
func (f *fakeClient) ContainerExecCreate(_ context.Context, containerID string, config container.ExecOptions) (container.ExecCreateResponse, error) { func (f *fakeClient) ContainerExecCreate(_ context.Context, containerID string, config container.ExecOptions) (types.IDResponse, error) {
if f.execCreateFunc != nil { if f.execCreateFunc != nil {
return f.execCreateFunc(containerID, config) return f.execCreateFunc(containerID, config)
} }
return container.ExecCreateResponse{}, nil return types.IDResponse{}, nil
} }
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (container.ExecInspect, error) { func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (container.ExecInspect, error) {
@ -75,7 +69,7 @@ func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (con
return container.ExecInspect{}, nil return container.ExecInspect{}, nil
} }
func (*fakeClient) ContainerExecStart(context.Context, string, container.ExecStartOptions) error { func (f *fakeClient) ContainerExecStart(context.Context, string, container.ExecStartOptions) error {
return nil return nil
} }
@ -84,7 +78,7 @@ func (f *fakeClient) ContainerCreate(
config *container.Config, config *container.Config,
hostConfig *container.HostConfig, hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig, networkingConfig *network.NetworkingConfig,
platform *ocispec.Platform, platform *specs.Platform,
containerName string, containerName string,
) (container.CreateResponse, error) { ) (container.CreateResponse, error) {
if f.createContainerFunc != nil { if f.createContainerFunc != nil {
@ -100,9 +94,9 @@ func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, op
return nil return nil
} }
func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) { func (f *fakeClient) ImageCreate(_ context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
if f.imageCreateFunc != nil { if f.imageCreateFunc != nil {
return f.imageCreateFunc(ctx, parentReference, options) return f.imageCreateFunc(parentReference, options)
} }
return nil, nil return nil, nil
} }
@ -181,54 +175,9 @@ func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.A
return container.PruneReport{}, nil return container.PruneReport{}, nil
} }
func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error {
if f.containerRestartFunc != nil {
return f.containerRestartFunc(ctx, containerID, options)
}
return nil
}
func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error {
if f.containerStopFunc != nil {
return f.containerStopFunc(ctx, containerID, options)
}
return nil
}
func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) { func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
if f.containerAttachFunc != nil { if f.containerAttachFunc != nil {
return f.containerAttachFunc(ctx, containerID, options) return f.containerAttachFunc(ctx, containerID, options)
} }
return types.HijackedResponse{}, nil return types.HijackedResponse{}, nil
} }
func (f *fakeClient) ContainerDiff(ctx context.Context, containerID string) ([]container.FilesystemChange, error) {
if f.containerDiffFunc != nil {
return f.containerDiffFunc(ctx, containerID)
}
return []container.FilesystemChange{}, nil
}
func (f *fakeClient) ContainerRename(ctx context.Context, oldName, newName string) error {
if f.containerRenameFunc != nil {
return f.containerRenameFunc(ctx, oldName, newName)
}
return nil
}
func (f *fakeClient) ContainerCommit(ctx context.Context, containerID string, options container.CommitOptions) (container.CommitResponse, error) {
if f.containerCommitFunc != nil {
return f.containerCommitFunc(ctx, containerID, options)
}
return container.CommitResponse{}, nil
}
func (f *fakeClient) ContainerPause(ctx context.Context, containerID string) error {
if f.containerPauseFunc != nil {
return f.containerPauseFunc(ctx, containerID)
}
return nil
}

View File

@ -28,7 +28,7 @@ func NewContainerCommand(dockerCli command.Cli) *cobra.Command {
NewPortCommand(dockerCli), NewPortCommand(dockerCli),
NewRenameCommand(dockerCli), NewRenameCommand(dockerCli),
NewRestartCommand(dockerCli), NewRestartCommand(dockerCli),
newRemoveCommand(dockerCli), NewRmCommand(dockerCli),
NewRunCommand(dockerCli), NewRunCommand(dockerCli),
NewStartCommand(dockerCli), NewStartCommand(dockerCli),
NewStatsCommand(dockerCli), NewStatsCommand(dockerCli),

View File

@ -8,7 +8,7 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/moby/moby/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -61,7 +61,7 @@ func runCommit(ctx context.Context, dockerCli command.Cli, options *commitOption
Reference: options.reference, Reference: options.reference,
Comment: options.comment, Comment: options.comment,
Author: options.author, Author: options.author,
Changes: options.changes.GetSlice(), Changes: options.changes.GetAll(),
Pause: options.pause, Pause: options.pause,
}) })
if err != nil { if err != nil {

View File

@ -1,70 +0,0 @@
package container
import (
"context"
"errors"
"io"
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/container"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestRunCommit(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerCommitFunc: func(
ctx context.Context,
ctr string,
options container.CommitOptions,
) (container.CommitResponse, error) {
assert.Check(t, is.Equal(options.Author, "Author Name <author@name.com>"))
assert.Check(t, is.DeepEqual(options.Changes, []string{"EXPOSE 80"}))
assert.Check(t, is.Equal(options.Comment, "commit message"))
assert.Check(t, is.Equal(options.Pause, false))
assert.Check(t, is.Equal(ctr, "container-id"))
return container.CommitResponse{ID: "image-id"}, nil
},
})
cmd := NewCommitCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetArgs(
[]string{
"--author", "Author Name <author@name.com>",
"--change", "EXPOSE 80",
"--message", "commit message",
"--pause=false",
"container-id",
},
)
err := cmd.Execute()
assert.NilError(t, err)
assert.Assert(t, is.Equal(cli.OutBuffer().String(), "image-id\n"))
}
func TestRunCommitClientError(t *testing.T) {
clientError := errors.New("client error")
cli := test.NewFakeCli(&fakeClient{
containerCommitFunc: func(
ctx context.Context,
ctr string,
options container.CommitOptions,
) (container.CommitResponse, error) {
return container.CommitResponse{}, clientError
},
})
cmd := NewCommitCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{"container-id"})
err := cmd.Execute()
assert.ErrorIs(t, err, clientError)
}

View File

@ -1,110 +1,70 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.23
package container package container
import ( import (
"strings"
"sync"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/moby/moby/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/moby/sys/capability"
"github.com/moby/sys/signal" "github.com/moby/sys/signal"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// allCaps is the magic value for "all capabilities".
const allCaps = "ALL"
// allLinuxCapabilities is a list of all known Linux capabilities. // 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") // TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true")
// TODO(thaJeztah): consider what casing we want to use for completion (see below); var allLinuxCapabilities = []string{
// "ALL", // magic value for "all capabilities"
// We need to consider what format is most convenient; currently we use the
// canonical name (uppercase and "CAP_" prefix), however, tab-completion is
// case-sensitive by default, so requires the user to type uppercase letters
// to filter the list of options.
//
// Bash completion provides a `completion-ignore-case on` option to make completion
// case-insensitive (https://askubuntu.com/a/87066), but it looks to be a global
// option; the current cobra.CompletionOptions also don't provide this as an option
// to be used in the generated completion-script.
//
// Fish completion has `smartcase` (by default?) which matches any case if
// all of the input is lowercase.
//
// Zsh does not appear have a dedicated option, but allows setting matching-rules
// (see https://superuser.com/a/1092328).
var allLinuxCapabilities = sync.OnceValue(func() []string {
caps := capability.ListKnown()
out := make([]string, 0, len(caps)+1)
out = append(out, allCaps)
for _, c := range caps {
out = append(out, "CAP_"+strings.ToUpper(c.String()))
}
return out
})
// logDriverOptions provides the options for each built-in logging driver. // caps35 is the caps of kernel 3.5 (37 entries)
var logDriverOptions = map[string][]string{ "CAP_CHOWN", // 2.2
"awslogs": { "CAP_DAC_OVERRIDE", // 2.2
"max-buffer-size", "mode", "awslogs-create-group", "awslogs-credentials-endpoint", "awslogs-datetime-format", "CAP_DAC_READ_SEARCH", // 2.2
"awslogs-group", "awslogs-multiline-pattern", "awslogs-region", "awslogs-stream", "tag", "CAP_FOWNER", // 2.2
}, "CAP_FSETID", // 2.2
"fluentd": { "CAP_KILL", // 2.2
"max-buffer-size", "mode", "env", "env-regex", "labels", "fluentd-address", "fluentd-async", "CAP_SETGID", // 2.2
"fluentd-buffer-limit", "fluentd-request-ack", "fluentd-retry-wait", "fluentd-max-retries", "CAP_SETUID", // 2.2
"fluentd-sub-second-precision", "fluentd-write-timeout", "tag", "CAP_SETPCAP", // 2.2
}, "CAP_LINUX_IMMUTABLE", // 2.2
"gcplogs": { "CAP_NET_BIND_SERVICE", // 2.2
"max-buffer-size", "mode", "env", "env-regex", "labels", "gcp-log-cmd", "gcp-meta-id", "gcp-meta-name", "CAP_NET_BROADCAST", // 2.2
"gcp-meta-zone", "gcp-project", "CAP_NET_ADMIN", // 2.2
}, "CAP_NET_RAW", // 2.2
"gelf": { "CAP_IPC_LOCK", // 2.2
"max-buffer-size", "mode", "env", "env-regex", "labels", "gelf-address", "gelf-compression-level", "CAP_IPC_OWNER", // 2.2
"gelf-compression-type", "gelf-tcp-max-reconnect", "gelf-tcp-reconnect-delay", "tag", "CAP_SYS_MODULE", // 2.2
}, "CAP_SYS_RAWIO", // 2.2
"journald": {"max-buffer-size", "mode", "env", "env-regex", "labels", "tag"}, "CAP_SYS_CHROOT", // 2.2
"json-file": {"max-buffer-size", "mode", "env", "env-regex", "labels", "compress", "max-file", "max-size"}, "CAP_SYS_PTRACE", // 2.2
"local": {"max-buffer-size", "mode", "compress", "max-file", "max-size"}, "CAP_SYS_PACCT", // 2.2
"none": {}, "CAP_SYS_ADMIN", // 2.2
"splunk": { "CAP_SYS_BOOT", // 2.2
"max-buffer-size", "mode", "env", "env-regex", "labels", "splunk-caname", "splunk-capath", "splunk-format", "CAP_SYS_NICE", // 2.2
"splunk-gzip", "splunk-gzip-level", "splunk-index", "splunk-insecureskipverify", "splunk-source", "CAP_SYS_RESOURCE", // 2.2
"splunk-sourcetype", "splunk-token", "splunk-url", "splunk-verify-connection", "tag", "CAP_SYS_TIME", // 2.2
}, "CAP_SYS_TTY_CONFIG", // 2.2
"syslog": { "CAP_MKNOD", // 2.4
"max-buffer-size", "mode", "env", "env-regex", "labels", "syslog-address", "syslog-facility", "syslog-format", "CAP_LEASE", // 2.4
"syslog-tls-ca-cert", "syslog-tls-cert", "syslog-tls-key", "syslog-tls-skip-verify", "tag", "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
// builtInLogDrivers provides a list of the built-in logging drivers. // caps316 is the caps of kernel 3.16 (38 entries)
var builtInLogDrivers = sync.OnceValue(func() []string { "CAP_AUDIT_READ",
drivers := make([]string, 0, len(logDriverOptions))
for driver := range logDriverOptions {
drivers = append(drivers, driver)
}
return drivers
})
// allLogDriverOptions provides all options of the built-in logging drivers. // caps58 is the caps of kernel 5.8 (40 entries)
// The list does not contain duplicates. "CAP_PERFMON",
var allLogDriverOptions = sync.OnceValue(func() []string { "CAP_BPF",
var result []string
seen := make(map[string]bool) // caps59 is the caps of kernel 5.9 (41 entries)
for driver := range logDriverOptions { "CAP_CHECKPOINT_RESTORE",
for _, opt := range logDriverOptions[driver] {
if !seen[opt] {
seen[opt] = true
result = append(result, opt)
} }
}
}
return result
})
// restartPolicies is a list of all valid restart-policies.. // restartPolicies is a list of all valid restart-policies..
// //
@ -116,209 +76,8 @@ var restartPolicies = []string{
string(container.RestartPolicyUnlessStopped), string(container.RestartPolicyUnlessStopped),
} }
// addCompletions adds the completions that `run` and `create` have in common.
func addCompletions(cmd *cobra.Command, dockerCLI completion.APIClientProvider) {
_ = cmd.RegisterFlagCompletionFunc("attach", completion.FromList("stderr", "stdin", "stdout"))
_ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
_ = cmd.RegisterFlagCompletionFunc("cgroupns", completeCgroupns())
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
_ = cmd.RegisterFlagCompletionFunc("ipc", completeIpc(dockerCLI))
_ = cmd.RegisterFlagCompletionFunc("link", completeLink(dockerCLI))
_ = cmd.RegisterFlagCompletionFunc("log-driver", completeLogDriver(dockerCLI))
_ = cmd.RegisterFlagCompletionFunc("log-opt", completeLogOpt)
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCLI))
_ = cmd.RegisterFlagCompletionFunc("pid", completePid(dockerCLI))
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
_ = cmd.RegisterFlagCompletionFunc("security-opt", completeSecurityOpt)
_ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
_ = cmd.RegisterFlagCompletionFunc("storage-opt", completeStorageOpt)
_ = cmd.RegisterFlagCompletionFunc("ulimit", completeUlimit)
_ = cmd.RegisterFlagCompletionFunc("userns", completion.FromList("host"))
_ = cmd.RegisterFlagCompletionFunc("uts", completion.FromList("host"))
_ = cmd.RegisterFlagCompletionFunc("volume-driver", completeVolumeDriver(dockerCLI))
_ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCLI, true))
}
// completeCgroupns implements shell completion for the `--cgroupns` option of `run` and `create`.
func completeCgroupns() cobra.CompletionFunc {
return completion.FromList(string(container.CgroupnsModeHost), string(container.CgroupnsModePrivate))
}
// completeDetachKeys implements shell completion for the `--detach-keys` option of `run` and `create`.
func completeDetachKeys(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return []string{"ctrl-"}, cobra.ShellCompDirectiveNoSpace
}
// completeIpc implements shell completion for the `--ipc` option of `run` and `create`.
// The completion is partly composite.
func completeIpc(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container"
return []string{"container:"}, cobra.ShellCompDirectiveNoSpace
}
if strings.HasPrefix(toComplete, "container:") {
names, _ := completion.ContainerNames(dockerCLI, true)(cmd, args, toComplete)
return prefixWith("container:", names), cobra.ShellCompDirectiveNoFileComp
}
return []string{
string(container.IPCModeContainer + ":"),
string(container.IPCModeHost),
string(container.IPCModeNone),
string(container.IPCModePrivate),
string(container.IPCModeShareable),
}, cobra.ShellCompDirectiveNoFileComp
}
}
// completeLink implements shell completion for the `--link` option of `run` and `create`.
func completeLink(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return postfixWith(":", containerNames(dockerCLI, cmd, args, toComplete)), cobra.ShellCompDirectiveNoSpace
}
}
// completeLogDriver implements shell completion for the `--log-driver` option of `run` and `create`.
// The log drivers are collected from a call to the Info endpoint with a fallback to a hard-coded list
// of the build-in log drivers.
func completeLogDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
info, err := dockerCLI.Client().Info(cmd.Context())
if err != nil {
return builtInLogDrivers(), cobra.ShellCompDirectiveNoFileComp
}
drivers := info.Plugins.Log
return drivers, cobra.ShellCompDirectiveNoFileComp
}
}
// completeLogOpt implements shell completion for the `--log-opt` option of `run` and `create`.
// If the user supplied a log-driver, only options for that driver are returned.
func completeLogOpt(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
driver, _ := cmd.Flags().GetString("log-driver")
if options, exists := logDriverOptions[driver]; exists {
return postfixWith("=", options), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
}
return postfixWith("=", allLogDriverOptions()), cobra.ShellCompDirectiveNoSpace
}
// completePid implements shell completion for the `--pid` option of `run` and `create`.
func completePid(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container"
return []string{"container:"}, cobra.ShellCompDirectiveNoSpace
}
if strings.HasPrefix(toComplete, "container:") {
names, _ := completion.ContainerNames(dockerCLI, true)(cmd, args, toComplete)
return prefixWith("container:", names), cobra.ShellCompDirectiveNoFileComp
}
return []string{"container:", "host"}, cobra.ShellCompDirectiveNoFileComp
}
}
// completeSecurityOpt implements shell completion for the `--security-opt` option of `run` and `create`.
// The completion is partly composite.
func completeSecurityOpt(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(toComplete) > 0 && strings.HasPrefix("apparmor=", toComplete) { //nolint:gocritic // not swapped, matches partly typed "apparmor="
return []string{"apparmor="}, cobra.ShellCompDirectiveNoSpace
}
if len(toComplete) > 0 && strings.HasPrefix("label", toComplete) { //nolint:gocritic // not swapped, matches partly typed "label"
return []string{"label="}, cobra.ShellCompDirectiveNoSpace
}
if strings.HasPrefix(toComplete, "label=") {
if strings.HasPrefix(toComplete, "label=d") {
return []string{"label=disable"}, cobra.ShellCompDirectiveNoFileComp
}
labels := []string{"disable", "level:", "role:", "type:", "user:"}
return prefixWith("label=", labels), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
}
// length must be > 1 here so that completion of "s" falls through.
if len(toComplete) > 1 && strings.HasPrefix("seccomp", toComplete) { //nolint:gocritic // not swapped, matches partly typed "seccomp"
return []string{"seccomp="}, cobra.ShellCompDirectiveNoSpace
}
if strings.HasPrefix(toComplete, "seccomp=") {
return []string{"seccomp=unconfined"}, cobra.ShellCompDirectiveNoFileComp
}
return []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"}, cobra.ShellCompDirectiveNoFileComp
}
// completeStorageOpt implements shell completion for the `--storage-opt` option of `run` and `create`.
func completeStorageOpt(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return []string{"size="}, cobra.ShellCompDirectiveNoSpace
}
// completeUlimit implements shell completion for the `--ulimit` option of `run` and `create`.
func completeUlimit(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
limits := []string{
"as",
"chroot",
"core",
"cpu",
"data",
"fsize",
"locks",
"maxlogins",
"maxsyslogins",
"memlock",
"msgqueue",
"nice",
"nofile",
"nproc",
"priority",
"rss",
"rtprio",
"sigpending",
"stack",
}
return postfixWith("=", limits), cobra.ShellCompDirectiveNoSpace
}
// completeVolumeDriver contacts the API to get the built-in and installed volume drivers.
func completeVolumeDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
info, err := dockerCLI.Client().Info(cmd.Context())
if err != nil {
// fallback: the built-in drivers
return []string{"local"}, cobra.ShellCompDirectiveNoFileComp
}
drivers := info.Plugins.Volume
return drivers, cobra.ShellCompDirectiveNoFileComp
}
}
// containerNames contacts the API to get names and optionally IDs of containers.
// In case of an error, an empty list is returned.
func containerNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command, args []string, toComplete string) []string {
names, _ := completion.ContainerNames(dockerCLI, true)(cmd, args, toComplete)
if names == nil {
return []string{}
}
return names
}
// prefixWith prefixes every element in the slice with the given prefix.
func prefixWith(prefix string, values []string) []string {
result := make([]string, len(values))
for i, v := range values {
result[i] = prefix + v
}
return result
}
// postfixWith appends postfix to every element in the slice.
func postfixWith(postfix string, values []string) []string {
result := make([]string, len(values))
for i, v := range values {
result[i] = v + postfix
}
return result
}
func completeLinuxCapabilityNames(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) { func completeLinuxCapabilityNames(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
return completion.FromList(allLinuxCapabilities()...)(cmd, args, toComplete) return completion.FromList(allLinuxCapabilities...)(cmd, args, toComplete)
} }
func completeRestartPolicies(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) { func completeRestartPolicies(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {

View File

@ -1,134 +0,0 @@
package container
import (
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/builders"
"github.com/moby/moby/api/types/container"
"github.com/moby/sys/signal"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestCompleteLinuxCapabilityNames(t *testing.T) {
names, directives := completeLinuxCapabilityNames(nil, nil, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
assert.Assert(t, len(names) > 1)
assert.Check(t, names[0] == allCaps)
for _, name := range names[1:] {
assert.Check(t, strings.HasPrefix(name, "CAP_"))
assert.Check(t, is.Equal(name, strings.ToUpper(name)), "Should be formatted uppercase")
}
}
func TestCompletePid(t *testing.T) {
tests := []struct {
containerListFunc func(container.ListOptions) ([]container.Summary, error)
toComplete string
expectedCompletions []string
expectedDirective cobra.ShellCompDirective
}{
{
toComplete: "",
expectedCompletions: []string{"container:", "host"},
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
toComplete: "c",
expectedCompletions: []string{"container:"},
expectedDirective: cobra.ShellCompDirectiveNoSpace,
},
{
containerListFunc: func(container.ListOptions) ([]container.Summary, error) {
return []container.Summary{
*builders.Container("c1"),
*builders.Container("c2"),
}, nil
},
toComplete: "container:",
expectedCompletions: []string{"container:c1", "container:c2"},
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
},
}
for _, tc := range tests {
t.Run(tc.toComplete, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerListFunc: tc.containerListFunc,
})
completions, directive := completePid(cli)(NewRunCommand(cli), nil, tc.toComplete)
assert.Check(t, is.DeepEqual(completions, tc.expectedCompletions))
assert.Check(t, is.Equal(directive, tc.expectedDirective))
})
}
}
func TestCompleteRestartPolicies(t *testing.T) {
values, directives := completeRestartPolicies(nil, nil, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
expected := restartPolicies
assert.Check(t, is.DeepEqual(values, expected))
}
func TestCompleteSecurityOpt(t *testing.T) {
tests := []struct {
toComplete string
expectedCompletions []string
expectedDirective cobra.ShellCompDirective
}{
{
toComplete: "",
expectedCompletions: []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"},
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
toComplete: "apparmor=",
expectedCompletions: []string{"apparmor="},
expectedDirective: cobra.ShellCompDirectiveNoSpace,
},
{
toComplete: "label=",
expectedCompletions: []string{"label=disable", "label=level:", "label=role:", "label=type:", "label=user:"},
expectedDirective: cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp,
},
{
toComplete: "s",
// We do not filter matching completions but delegate this task to the shell script.
expectedCompletions: []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"},
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
toComplete: "se",
expectedCompletions: []string{"seccomp="},
expectedDirective: cobra.ShellCompDirectiveNoSpace,
},
{
toComplete: "seccomp=",
expectedCompletions: []string{"seccomp=unconfined"},
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
toComplete: "sy",
expectedCompletions: []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"},
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
},
}
for _, tc := range tests {
t.Run(tc.toComplete, func(t *testing.T) {
completions, directive := completeSecurityOpt(nil, nil, tc.toComplete)
assert.Check(t, is.DeepEqual(completions, tc.expectedCompletions))
assert.Check(t, is.Equal(directive, tc.expectedDirective))
})
}
}
func TestCompleteSignals(t *testing.T) {
values, directives := completeSignals(nil, nil, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
assert.Check(t, len(values) > 1)
assert.Check(t, is.Len(values, len(signal.SignalMap)))
}

View File

@ -15,9 +15,10 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/docker/go-units" "github.com/docker/docker/api/types/container"
"github.com/moby/go-archive" "github.com/docker/docker/pkg/archive"
"github.com/moby/moby/api/types/container" "github.com/docker/docker/pkg/system"
units "github.com/docker/go-units"
"github.com/morikuni/aec" "github.com/morikuni/aec"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -129,12 +130,13 @@ func NewCopyCommand(dockerCli command.Cli) *cobra.Command {
Use: `cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|- Use: `cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-
docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH`, docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH`,
Short: "Copy files/folders between a container and the local filesystem", Short: "Copy files/folders between a container and the local filesystem",
Long: `Copy files/folders between a container and the local filesystem Long: strings.Join([]string{
"Copy files/folders between a container and the local filesystem\n",
Use '-' as the source to read a tar archive from stdin "\nUse '-' as the source to read a tar archive from stdin\n",
and extract it to a directory destination in a container. "and extract it to a directory destination in a container.\n",
Use '-' as the destination to stream a tar archive of a "Use '-' as the destination to stream a tar archive of a\n",
container source to stdout.`, "container source to stdout.",
}, ""),
Args: cli.ExactArgs(2), Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if args[0] == "" { if args[0] == "" {
@ -201,15 +203,14 @@ func runCopy(ctx context.Context, dockerCli command.Cli, opts copyOptions) error
} }
} }
func resolveLocalPath(localPath string) (absPath string, _ error) { func resolveLocalPath(localPath string) (absPath string, err error) {
absPath, err := filepath.Abs(localPath) if absPath, err = filepath.Abs(localPath); err != nil {
if err != nil { return
return "", err
} }
return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
} }
func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpConfig) (err error) { func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
dstPath := copyConfig.destPath dstPath := copyConfig.destPath
srcPath := copyConfig.sourcePath srcPath := copyConfig.sourcePath
@ -225,16 +226,16 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
return err return err
} }
apiClient := dockerCLI.Client() client := dockerCli.Client()
// if client requests to follow symbol link, then must decide target file to be copied // if client requests to follow symbol link, then must decide target file to be copied
var rebaseName string var rebaseName string
if copyConfig.followLink { if copyConfig.followLink {
srcStat, err := apiClient.ContainerStatPath(ctx, copyConfig.container, srcPath) srcStat, err := client.ContainerStatPath(ctx, copyConfig.container, srcPath)
// If the destination is a symbolic link, we should follow it. // If the destination is a symbolic link, we should follow it.
if err == nil && srcStat.Mode&os.ModeSymlink != 0 { if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
linkTarget := srcStat.LinkTarget linkTarget := srcStat.LinkTarget
if !isAbs(linkTarget) { if !system.IsAbs(linkTarget) {
// Join with the parent directory. // Join with the parent directory.
srcParent, _ := archive.SplitPathDirEntry(srcPath) srcParent, _ := archive.SplitPathDirEntry(srcPath)
linkTarget = filepath.Join(srcParent, linkTarget) linkTarget = filepath.Join(srcParent, linkTarget)
@ -248,14 +249,14 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
defer cancel() defer cancel()
content, stat, err := apiClient.CopyFromContainer(ctx, copyConfig.container, srcPath) content, stat, err := client.CopyFromContainer(ctx, copyConfig.container, srcPath)
if err != nil { if err != nil {
return err return err
} }
defer content.Close() defer content.Close()
if dstPath == "-" { if dstPath == "-" {
_, err = io.Copy(dockerCLI.Out(), content) _, err = io.Copy(dockerCli.Out(), content)
return err return err
} }
@ -284,12 +285,12 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
return archive.CopyTo(preArchive, srcInfo, dstPath) return archive.CopyTo(preArchive, srcInfo, dstPath)
} }
restore, done := copyProgress(ctx, dockerCLI.Err(), copyFromContainerHeader, &copiedSize) restore, done := copyProgress(ctx, dockerCli.Err(), copyFromContainerHeader, &copiedSize)
res := archive.CopyTo(preArchive, srcInfo, dstPath) res := archive.CopyTo(preArchive, srcInfo, dstPath)
cancel() cancel()
<-done <-done
restore() restore()
_, _ = fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", dstPath) fmt.Fprintln(dockerCli.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", dstPath)
return res return res
} }
@ -298,7 +299,7 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
// about both the source and destination. The API is a simple tar // about both the source and destination. The API is a simple tar
// archive/extract API but we can use the stat info header about the // archive/extract API but we can use the stat info header about the
// destination to be more informed about exactly what the destination is. // destination to be more informed about exactly what the destination is.
func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpConfig) (err error) { func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
srcPath := copyConfig.sourcePath srcPath := copyConfig.sourcePath
dstPath := copyConfig.destPath dstPath := copyConfig.destPath
@ -310,23 +311,22 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
} }
} }
apiClient := dockerCLI.Client() client := dockerCli.Client()
// Prepare destination copy info by stat-ing the container path. // Prepare destination copy info by stat-ing the container path.
dstInfo := archive.CopyInfo{Path: dstPath} dstInfo := archive.CopyInfo{Path: dstPath}
dstStat, err := apiClient.ContainerStatPath(ctx, copyConfig.container, dstPath) dstStat, err := client.ContainerStatPath(ctx, copyConfig.container, dstPath)
// If the destination is a symbolic link, we should evaluate it. // If the destination is a symbolic link, we should evaluate it.
if err == nil && dstStat.Mode&os.ModeSymlink != 0 { if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
linkTarget := dstStat.LinkTarget linkTarget := dstStat.LinkTarget
if !isAbs(linkTarget) { if !system.IsAbs(linkTarget) {
// Join with the parent directory. // Join with the parent directory.
dstParent, _ := archive.SplitPathDirEntry(dstPath) dstParent, _ := archive.SplitPathDirEntry(dstPath)
linkTarget = filepath.Join(dstParent, linkTarget) linkTarget = filepath.Join(dstParent, linkTarget)
} }
dstInfo.Path = linkTarget dstInfo.Path = linkTarget
dstStat, err = apiClient.ContainerStatPath(ctx, copyConfig.container, linkTarget) dstStat, err = client.ContainerStatPath(ctx, copyConfig.container, linkTarget)
// FIXME(thaJeztah): unhandled error (should this return?)
} }
// Validate the destination path // Validate the destination path
@ -398,20 +398,21 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
} }
options := container.CopyToContainerOptions{ options := container.CopyToContainerOptions{
AllowOverwriteDirWithFile: false,
CopyUIDGID: copyConfig.copyUIDGID, CopyUIDGID: copyConfig.copyUIDGID,
} }
if copyConfig.quiet { if copyConfig.quiet {
return apiClient.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options) return client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
} }
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
restore, done := copyProgress(ctx, dockerCLI.Err(), copyToContainerHeader, &copiedSize) restore, done := copyProgress(ctx, dockerCli.Err(), copyToContainerHeader, &copiedSize)
res := apiClient.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options) res := client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
cancel() cancel()
<-done <-done
restore() restore()
fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path) fmt.Fprintln(dockerCli.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path)
return res return res
} }
@ -433,7 +434,7 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
// client, a `:` could be part of an absolute Windows path, in which case it // client, a `:` could be part of an absolute Windows path, in which case it
// is immediately proceeded by a backslash. // is immediately proceeded by a backslash.
func splitCpArg(arg string) (ctr, path string) { func splitCpArg(arg string) (ctr, path string) {
if isAbs(arg) { if system.IsAbs(arg) {
// Explicit local absolute path, e.g., `C:\foo` or `/foo`. // Explicit local absolute path, e.g., `C:\foo` or `/foo`.
return "", arg return "", arg
} }
@ -447,15 +448,3 @@ func splitCpArg(arg string) (ctr, path string) {
return ctr, path return ctr, path
} }
// IsAbs is a platform-agnostic wrapper for filepath.IsAbs.
//
// On Windows, golang filepath.IsAbs does not consider a path \windows\system32
// as absolute as it doesn't start with a drive-letter/colon combination. However,
// in docker we need to verify things such as WORKDIR /windows/system32 in
// a Dockerfile (which gets translated to \windows\system32 when being processed
// by the daemon). This SHOULD be treated as absolute from a docker processing
// perspective.
func isAbs(path string) bool {
return filepath.IsAbs(path) || strings.HasPrefix(path, string(os.PathSeparator))
}

View File

@ -9,12 +9,12 @@ import (
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/moby/go-archive" "github.com/docker/docker/api/types/container"
"github.com/moby/go-archive/compression" "github.com/docker/docker/pkg/archive"
"github.com/moby/moby/api/types/container"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/fs" "gotest.tools/v3/fs"
"gotest.tools/v3/skip"
) )
func TestRunCopyWithInvalidArguments(t *testing.T) { func TestRunCopyWithInvalidArguments(t *testing.T) {
@ -67,15 +67,14 @@ func TestRunCopyFromContainerToStdout(t *testing.T) {
} }
func TestRunCopyFromContainerToFilesystem(t *testing.T) { func TestRunCopyFromContainerToFilesystem(t *testing.T) {
srcDir := fs.NewDir(t, "cp-test", destDir := fs.NewDir(t, "cp-test",
fs.WithFile("file1", "content\n")) fs.WithFile("file1", "content\n"))
defer destDir.Remove()
destDir := fs.NewDir(t, "cp-test")
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) { containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
assert.Check(t, is.Equal("container", ctr)) assert.Check(t, is.Equal("container", ctr))
readCloser, err := archive.Tar(srcDir.Path(), compression.None) readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
return readCloser, container.PathStat{}, err return readCloser, container.PathStat{}, err
}, },
}) })
@ -152,7 +151,7 @@ func TestSplitCpArg(t *testing.T) {
}{ }{
{ {
doc: "absolute path with colon", doc: "absolute path with colon",
os: "unix", os: "linux",
path: "/abs/path:withcolon", path: "/abs/path:withcolon",
expectedPath: "/abs/path:withcolon", expectedPath: "/abs/path:withcolon",
}, },
@ -180,13 +179,9 @@ func TestSplitCpArg(t *testing.T) {
}, },
} }
for _, tc := range testcases { for _, tc := range testcases {
tc := tc
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
if tc.os == "windows" && runtime.GOOS != "windows" { skip.If(t, tc.os == "windows" && runtime.GOOS != "windows" || tc.os == "linux" && runtime.GOOS == "windows")
t.Skip("skipping windows test on non-windows platform")
}
if tc.os == "unix" && runtime.GOOS == "windows" {
t.Skip("skipping unix test on windows")
}
ctr, path := splitCpArg(tc.path) ctr, path := splitCpArg(tc.path)
assert.Check(t, is.Equal(tc.expectedContainer, ctr)) assert.Check(t, is.Equal(tc.expectedContainer, ctr))

View File

@ -1,35 +1,26 @@
package container package container
import ( import (
"archive/tar"
"bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
"net/netip"
"os" "os"
"path" "regexp"
"strings"
cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/platforms" "github.com/containerd/platforms"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/jsonstream"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/moby/moby/api/types/container" "github.com/docker/docker/api/types/container"
imagetypes "github.com/moby/moby/api/types/image" imagetypes "github.com/docker/docker/api/types/image"
"github.com/moby/moby/api/types/mount" "github.com/docker/docker/api/types/versions"
"github.com/moby/moby/api/types/versions" "github.com/docker/docker/errdefs"
"github.com/moby/moby/client" "github.com/docker/docker/pkg/jsonmessage"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -48,7 +39,6 @@ type createOptions struct {
untrusted bool untrusted bool
pull string // always, missing, never pull string // always, missing, never
quiet bool quiet bool
useAPISocket bool
} }
// NewCreateCommand creates a new cobra.Command for `docker create` // NewCreateCommand creates a new cobra.Command for `docker create`
@ -70,7 +60,7 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container create, docker create", "aliases": "docker container create, docker create",
}, },
ValidArgsFunction: completion.ImageNames(dockerCli, -1), ValidArgsFunction: completion.ImageNames(dockerCli),
} }
flags := cmd.Flags() flags := cmd.Flags()
@ -79,8 +69,6 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
flags.StringVar(&options.name, "name", "", "Assign a name to the container") flags.StringVar(&options.name, "name", "", "Assign a name to the container")
flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before creating ("`+PullImageAlways+`", "|`+PullImageMissing+`", "`+PullImageNever+`")`) flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before creating ("`+PullImageAlways+`", "|`+PullImageMissing+`", "`+PullImageNever+`")`)
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output") flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output")
flags.BoolVarP(&options.useAPISocket, "use-api-socket", "", false, "Bind mount Docker API socket and required auth")
flags.SetAnnotation("use-api-socket", "experimentalCLI", nil) // Marks flag as experimental for now.
// Add an explicit help that doesn't have a `-h` to prevent the conflict // Add an explicit help that doesn't have a `-h` to prevent the conflict
// with hostname // with hostname
@ -90,26 +78,24 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled()) command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled())
copts = addFlags(flags) copts = addFlags(flags)
addCompletions(cmd, dockerCli) _ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
flags.VisitAll(func(flag *pflag.Flag) { _ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
// Set a default completion function if none was set. We don't look _ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
// up if it does already have one set, because Cobra does this for _ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCli))
// us, and returns an error (which we ignore for this reason). _ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
_ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) _ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
}) _ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
_ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCli, true))
return cmd return cmd
} }
func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error { func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error {
if err := validatePullOpt(options.pull); err != nil { if err := validatePullOpt(options.pull); err != nil {
return cli.StatusError{ reportError(dockerCli.Err(), "create", err.Error(), true)
Status: withHelp(err, "create").Error(), return cli.StatusError{StatusCode: 125}
StatusCode: 125,
} }
} proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetAll()))
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetSlice()))
newEnv := []string{} newEnv := []string{}
for k, v := range proxyConfig { for k, v := range proxyConfig {
if v == nil { if v == nil {
@ -121,10 +107,12 @@ func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet,
copts.env = *opts.NewListOptsRef(&newEnv, nil) copts.env = *opts.NewListOptsRef(&newEnv, nil)
containerCfg, err := parse(flags, copts, dockerCli.ServerInfo().OSType) containerCfg, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
if err != nil { if err != nil {
return cli.StatusError{ reportError(dockerCli.Err(), "create", err.Error(), true)
Status: withHelp(err, "create").Error(), return cli.StatusError{StatusCode: 125}
StatusCode: 125,
} }
if err = validateAPIVersion(containerCfg, dockerCli.Client().ClientVersion()); err != nil {
reportError(dockerCli.Err(), "create", err.Error(), true)
return cli.StatusError{StatusCode: 125}
} }
id, err := createContainer(ctx, dockerCli, containerCfg, options) id, err := createContainer(ctx, dockerCli, containerCfg, options)
if err != nil { if err != nil {
@ -154,7 +142,7 @@ func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *
if options.quiet { if options.quiet {
out = streams.NewOut(io.Discard) out = streams.NewOut(io.Discard)
} }
return jsonstream.Display(ctx, responseBody, out) return jsonmessage.DisplayJSONMessagesToStream(responseBody, out, nil)
} }
type cidFile struct { type cidFile struct {
@ -190,20 +178,20 @@ func (cid *cidFile) Write(id string) error {
return nil return nil
} }
func newCIDFile(cidPath string) (*cidFile, error) { func newCIDFile(path string) (*cidFile, error) {
if cidPath == "" { if path == "" {
return &cidFile{}, nil return &cidFile{}, nil
} }
if _, err := os.Stat(cidPath); err == nil { if _, err := os.Stat(path); err == nil {
return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", cidPath) return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", path)
} }
f, err := os.Create(cidPath) f, err := os.Create(path)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to create the container ID file") return nil, errors.Wrap(err, "failed to create the container ID file")
} }
return &cidFile{path: cidPath, file: f}, nil return &cidFile{path: path, file: f}, nil
} }
//nolint:gocyclo //nolint:gocyclo
@ -212,6 +200,9 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
hostConfig := containerCfg.HostConfig hostConfig := containerCfg.HostConfig
networkingConfig := containerCfg.NetworkingConfig networkingConfig := containerCfg.NetworkingConfig
warnOnOomKillDisable(*hostConfig, dockerCli.Err())
warnOnLocalhostDNS(*hostConfig, dockerCli.Err())
var ( var (
trustedRef reference.Canonical trustedRef reference.Canonical
namedRef reference.Named namedRef reference.Named
@ -240,74 +231,17 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
} }
} }
const dockerConfigPathInContainer = "/run/secrets/docker/config.json" pullAndTagImage := func() error {
var apiSocketCreds map[string]types.AuthConfig if err := pullImage(ctx, dockerCli, config.Image, options); err != nil {
return err
if options.useAPISocket { }
// We'll create two new mounts to handle this flag: if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
// 1. Mount the actual docker socket. return image.TagTrusted(ctx, dockerCli, trustedRef, taggedRef)
// 2. A synthezised ~/.docker/config.json with resolved tokens. }
return nil
if dockerCli.ServerInfo().OSType == "windows" {
return "", errors.New("flag --use-api-socket can't be used with a Windows Docker Engine")
} }
// hard-code engine socket path until https://github.com/moby/moby/pull/43459 gives us a discovery mechanism var platform *specs.Platform
containerCfg.HostConfig.Mounts = append(containerCfg.HostConfig.Mounts, mount.Mount{
Type: mount.TypeBind,
Source: "/var/run/docker.sock",
Target: "/var/run/docker.sock",
BindOptions: &mount.BindOptions{},
})
/*
Ideally, we'd like to copy the config into a tmpfs but unfortunately,
the mounts won't be in place until we start the container. This can
leave around the config if the container doesn't get deleted.
We are using the most compose-secret-compatible approach,
which is implemented at
https://github.com/docker/compose/blob/main/pkg/compose/convergence.go#L737
// Prepare a tmpfs mount for our credentials so they go away after the
// container exits. We'll copy into this mount after the container is
// created.
containerCfg.HostConfig.Mounts = append(containerCfg.HostConfig.Mounts, mount.Mount{
Type: mount.TypeTmpfs,
Target: "/docker/",
TmpfsOptions: &mount.TmpfsOptions{
SizeBytes: 1 << 20, // only need a small partition
Mode: 0o600,
},
})
*/
var envvarPresent bool
for _, envvar := range containerCfg.Config.Env {
if strings.HasPrefix(envvar, "DOCKER_CONFIG=") {
envvarPresent = true
}
}
// If the DOCKER_CONFIG env var is already present, we assume the client knows
// what they're doing and don't inject the creds.
if !envvarPresent {
// Resolve this here for later, ensuring we error our before we create the container.
creds, err := readCredentials(dockerCli)
if err != nil {
return "", fmt.Errorf("resolving credentials failed: %w", err)
}
if len(creds) > 0 {
// Set our special little location for the config file.
containerCfg.Config.Env = append(containerCfg.Config.Env, "DOCKER_CONFIG="+path.Dir(dockerConfigPathInContainer))
apiSocketCreds = creds // inject these after container creation.
}
}
}
var platform *ocispec.Platform
// Engine API version 1.41 first introduced the option to specify platform on // Engine API version 1.41 first introduced the option to specify platform on
// create. It will produce an error if you try to set a platform on older API // create. It will produce an error if you try to set a platform on older API
// versions, so check the API version here to maintain backwards // versions, so check the API version here to maintain backwards
@ -315,21 +249,11 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
if options.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") { if options.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") {
p, err := platforms.Parse(options.platform) p, err := platforms.Parse(options.platform)
if err != nil { if err != nil {
return "", errors.Wrap(invalidParameter(err), "error parsing specified platform") return "", errors.Wrap(errdefs.InvalidParameter(err), "error parsing specified platform")
} }
platform = &p platform = &p
} }
pullAndTagImage := func() error {
if err := pullImage(ctx, dockerCli, config.Image, options); err != nil {
return err
}
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
return trust.TagTrusted(ctx, dockerCli.Client(), dockerCli.Err(), trustedRef, taggedRef)
}
return nil
}
if options.pull == PullImageAlways { if options.pull == PullImageAlways {
if err := pullAndTagImage(); err != nil { if err := pullAndTagImage(); err != nil {
return "", err return "", err
@ -341,10 +265,10 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name) response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
if err != nil { if err != nil {
// Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior. // Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior.
if cerrdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing { if errdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing {
if !options.quiet { if !options.quiet {
// we don't want to write to stdout anything apart from container.ID // we don't want to write to stdout anything apart from container.ID
_, _ = fmt.Fprintf(dockerCli.Err(), "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef)) fmt.Fprintf(dockerCli.Err(), "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
} }
if err := pullAndTagImage(); err != nil { if err := pullAndTagImage(); err != nil {
@ -361,41 +285,40 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
} }
} }
if warn := localhostDNSWarning(*hostConfig); warn != "" {
response.Warnings = append(response.Warnings, warn)
}
containerID = response.ID
for _, w := range response.Warnings { for _, w := range response.Warnings {
_, _ = fmt.Fprintln(dockerCli.Err(), "WARNING:", w) _, _ = fmt.Fprintf(dockerCli.Err(), "WARNING: %s\n", w)
} }
err = containerIDFile.Write(containerID) err = containerIDFile.Write(response.ID)
return response.ID, err
if options.useAPISocket && len(apiSocketCreds) > 0 {
// Create a new config file with just the auth.
newConfig := &configfile.ConfigFile{
AuthConfigs: apiSocketCreds,
} }
if err := copyDockerConfigIntoContainer(ctx, dockerCli.Client(), containerID, dockerConfigPathInContainer, newConfig); err != nil { func warnOnOomKillDisable(hostConfig container.HostConfig, stderr io.Writer) {
return "", fmt.Errorf("injecting docker config.json into container failed: %w", err) if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 {
fmt.Fprintln(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.")
} }
} }
return containerID, err
}
// check the DNS settings passed via --dns against localhost regexp to warn if // check the DNS settings passed via --dns against localhost regexp to warn if
// they are trying to set a DNS to a localhost address. // they are trying to set a DNS to a localhost address
// func warnOnLocalhostDNS(hostConfig container.HostConfig, stderr io.Writer) {
// TODO(thaJeztah): move this to the daemon, which can make a better call if it will work or not (depending on networking mode).
func localhostDNSWarning(hostConfig container.HostConfig) string {
for _, dnsIP := range hostConfig.DNS { for _, dnsIP := range hostConfig.DNS {
if addr, err := netip.ParseAddr(dnsIP); err == nil && addr.IsLoopback() { if isLocalhost(dnsIP) {
return fmt.Sprintf("Localhost DNS (%s) may fail in containers.", addr) fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP)
return
} }
} }
return "" }
// IPLocalhost is a regex pattern for IPv4 or IPv6 loopback range.
const ipLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)`
var localhostIPRegexp = regexp.MustCompile(ipLocalhost)
// IsLocalhost returns true if ip matches the localhost IP regular expression.
// Used for determining if nameserver settings are being passed which are
// localhost addresses
func isLocalhost(ip string) bool {
return localhostIPRegexp.MatchString(ip)
} }
func validatePullOpt(val string) error { func validatePullOpt(val string) error {
@ -413,39 +336,3 @@ func validatePullOpt(val string) error {
) )
} }
} }
// copyDockerConfigIntoContainer takes the client configuration and copies it
// into the container.
//
// The path should be an absolute path in the container, commonly
// /root/.docker/config.json.
func copyDockerConfigIntoContainer(ctx context.Context, dockerAPI client.APIClient, containerID string, configPath string, config *configfile.ConfigFile) error {
var configBuf bytes.Buffer
if err := config.SaveToWriter(&configBuf); err != nil {
return fmt.Errorf("saving creds: %w", err)
}
// We don't need to get super fancy with the tar creation.
var tarBuf bytes.Buffer
tarWriter := tar.NewWriter(&tarBuf)
tarWriter.WriteHeader(&tar.Header{
Name: configPath,
Size: int64(configBuf.Len()),
Mode: 0o600,
})
if _, err := io.Copy(tarWriter, &configBuf); err != nil {
return fmt.Errorf("writing config to tar file for config copy: %w", err)
}
if err := tarWriter.Close(); err != nil {
return fmt.Errorf("closing tar for config copy failed: %w", err)
}
if err := dockerAPI.CopyToContainer(ctx, containerID, "/",
&tarBuf, container.CopyToContainerOptions{}); err != nil {
return fmt.Errorf("copying config.json into container failed: %w", err)
}
return nil
}

View File

@ -14,12 +14,12 @@ import (
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary" "github.com/docker/cli/internal/test/notary"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/system"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/moby/moby/api/types/container" specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/system"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
@ -113,6 +113,7 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
}, },
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(tc.PullPolicy, func(t *testing.T) { t.Run(tc.PullPolicy, func(t *testing.T) {
pullCounter := 0 pullCounter := 0
@ -121,7 +122,7 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
config *container.Config, config *container.Config,
hostConfig *container.HostConfig, hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig, networkingConfig *network.NetworkingConfig,
platform *ocispec.Platform, platform *specs.Platform,
containerName string, containerName string,
) (container.CreateResponse, error) { ) (container.CreateResponse, error) {
defer func() { tc.ResponseCounter++ }() defer func() { tc.ResponseCounter++ }()
@ -132,7 +133,7 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
return container.CreateResponse{ID: containerID}, nil return container.CreateResponse{ID: containerID}, nil
} }
}, },
imageCreateFunc: func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) { imageCreateFunc: func(parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
defer func() { pullCounter++ }() defer func() { pullCounter++ }()
return io.NopCloser(strings.NewReader("")), nil return io.NopCloser(strings.NewReader("")), nil
}, },
@ -175,6 +176,7 @@ func TestCreateContainerImagePullPolicyInvalid(t *testing.T) {
}, },
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(tc.PullPolicy, func(t *testing.T) { t.Run(tc.PullPolicy, func(t *testing.T) {
dockerCli := test.NewFakeCli(&fakeClient{}) dockerCli := test.NewFakeCli(&fakeClient{})
err := runCreate( err := runCreate(
@ -187,36 +189,8 @@ func TestCreateContainerImagePullPolicyInvalid(t *testing.T) {
statusErr := cli.StatusError{} statusErr := cli.StatusError{}
assert.Check(t, errors.As(err, &statusErr)) assert.Check(t, errors.As(err, &statusErr))
assert.Check(t, is.Equal(statusErr.StatusCode, 125)) assert.Equal(t, statusErr.StatusCode, 125)
assert.Check(t, is.ErrorContains(err, tc.ExpectedErrMsg)) assert.Check(t, is.Contains(dockerCli.ErrBuffer().String(), tc.ExpectedErrMsg))
})
}
}
func TestCreateContainerValidateFlags(t *testing.T) {
for _, tc := range []struct {
name string
args []string
expectedErr string
}{
{
name: "with invalid --attach value",
args: []string{"--attach", "STDINFO", "myimage"},
expectedErr: `invalid argument "STDINFO" for "-a, --attach" flag: valid streams are STDIN, STDOUT and STDERR`,
},
} {
t.Run(tc.name, func(t *testing.T) {
cmd := NewCreateCommand(test.NewFakeCli(&fakeClient{}))
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
if tc.expectedErr != "" {
assert.Check(t, is.ErrorContains(err, tc.expectedErr))
} else {
assert.Check(t, is.Nil(err))
}
}) })
} }
} }
@ -248,12 +222,12 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { tc := tc
fakeCLI := test.NewFakeCli(&fakeClient{ fakeCLI := test.NewFakeCli(&fakeClient{
createContainerFunc: func(config *container.Config, createContainerFunc: func(config *container.Config,
hostConfig *container.HostConfig, hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig, networkingConfig *network.NetworkingConfig,
platform *ocispec.Platform, platform *specs.Platform,
containerName string, containerName string,
) (container.CreateResponse, error) { ) (container.CreateResponse, error) {
return container.CreateResponse{}, errors.New("shouldn't try to pull image") return container.CreateResponse{}, errors.New("shouldn't try to pull image")
@ -266,7 +240,6 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
err := cmd.Execute() err := cmd.Execute()
assert.ErrorContains(t, err, tc.expectedError) assert.ErrorContains(t, err, tc.expectedError)
})
} }
} }
@ -274,22 +247,29 @@ func TestNewCreateCommandWithWarnings(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
args []string args []string
warnings []string
warning bool warning bool
}{ }{
{ {
name: "container-create-no-warnings", name: "container-create-without-oom-kill-disable",
args: []string{"image:tag"}, args: []string{"image:tag"},
}, },
{ {
name: "container-create-daemon-single-warning", name: "container-create-oom-kill-disable-false",
args: []string{"image:tag"}, args: []string{"--oom-kill-disable=false", "image:tag"},
warnings: []string{"warning from daemon"},
}, },
{ {
name: "container-create-daemon-multiple-warnings", name: "container-create-oom-kill-without-memory-limit",
args: []string{"image:tag"}, args: []string{"--oom-kill-disable", "image:tag"},
warnings: []string{"warning from daemon", "another warning from daemon"}, warning: true,
},
{
name: "container-create-oom-kill-true-without-memory-limit",
args: []string{"--oom-kill-disable=true", "image:tag"},
warning: true,
},
{
name: "container-create-oom-kill-true-with-memory-limit",
args: []string{"--oom-kill-disable=true", "--memory=100M", "image:tag"},
}, },
{ {
name: "container-create-localhost-dns", name: "container-create-localhost-dns",
@ -303,26 +283,27 @@ func TestNewCreateCommandWithWarnings(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
fakeCLI := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
createContainerFunc: func(config *container.Config, createContainerFunc: func(config *container.Config,
hostConfig *container.HostConfig, hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig, networkingConfig *network.NetworkingConfig,
platform *ocispec.Platform, platform *specs.Platform,
containerName string, containerName string,
) (container.CreateResponse, error) { ) (container.CreateResponse, error) {
return container.CreateResponse{Warnings: tc.warnings}, nil return container.CreateResponse{}, nil
}, },
}) })
cmd := NewCreateCommand(fakeCLI) cmd := NewCreateCommand(cli)
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
err := cmd.Execute() err := cmd.Execute()
assert.NilError(t, err) assert.NilError(t, err)
if tc.warning || len(tc.warnings) > 0 { if tc.warning {
golden.Assert(t, fakeCLI.ErrBuffer().String(), tc.name+".golden") golden.Assert(t, cli.ErrBuffer().String(), tc.name+".golden")
} else { } else {
assert.Equal(t, fakeCLI.ErrBuffer().String(), "") assert.Equal(t, cli.ErrBuffer().String(), "")
} }
}) })
} }
@ -347,7 +328,7 @@ func TestCreateContainerWithProxyConfig(t *testing.T) {
createContainerFunc: func(config *container.Config, createContainerFunc: func(config *container.Config,
hostConfig *container.HostConfig, hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig, networkingConfig *network.NetworkingConfig,
platform *ocispec.Platform, platform *specs.Platform,
containerName string, containerName string,
) (container.CreateResponse, error) { ) (container.CreateResponse, error) {
sort.Strings(config.Env) sort.Strings(config.Env)
@ -375,5 +356,5 @@ func TestCreateContainerWithProxyConfig(t *testing.T) {
type fakeNotFound struct{} type fakeNotFound struct{}
func (fakeNotFound) NotFound() {} func (f fakeNotFound) NotFound() {}
func (fakeNotFound) Error() string { return "error fake not found" } func (f fakeNotFound) Error() string { return "error fake not found" }

View File

@ -7,17 +7,25 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type diffOptions struct {
container string
}
// NewDiffCommand creates a new cobra.Command for `docker diff` // NewDiffCommand creates a new cobra.Command for `docker diff`
func NewDiffCommand(dockerCli command.Cli) *cobra.Command { func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
var opts diffOptions
return &cobra.Command{ return &cobra.Command{
Use: "diff CONTAINER", Use: "diff CONTAINER",
Short: "Inspect changes to files or directories on a container's filesystem", Short: "Inspect changes to files or directories on a container's filesystem",
Args: cli.ExactArgs(1), Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runDiff(cmd.Context(), dockerCli, args[0]) opts.container = args[0]
return runDiff(cmd.Context(), dockerCli, &opts)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container diff, docker diff", "aliases": "docker container diff, docker diff",
@ -26,14 +34,17 @@ func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
} }
} }
func runDiff(ctx context.Context, dockerCLI command.Cli, containerID string) error { func runDiff(ctx context.Context, dockerCli command.Cli, opts *diffOptions) error {
changes, err := dockerCLI.Client().ContainerDiff(ctx, containerID) if opts.container == "" {
return errors.New("Container name cannot be empty")
}
changes, err := dockerCli.Client().ContainerDiff(ctx, opts.container)
if err != nil { if err != nil {
return err return err
} }
diffCtx := formatter.Context{ diffCtx := formatter.Context{
Output: dockerCLI.Out(), Output: dockerCli.Out(),
Format: newDiffFormat("{{.Type}} {{.Path}}"), Format: NewDiffFormat("{{.Type}} {{.Path}}"),
} }
return diffFormatWrite(diffCtx, changes) return DiffFormatWrite(diffCtx, changes)
} }

View File

@ -1,79 +0,0 @@
package container
import (
"context"
"errors"
"io"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/container"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestRunDiff(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerDiffFunc: func(
ctx context.Context,
containerID string,
) ([]container.FilesystemChange, error) {
return []container.FilesystemChange{
{
Kind: container.ChangeModify,
Path: "/path/to/file0",
},
{
Kind: container.ChangeAdd,
Path: "/path/to/file1",
},
{
Kind: container.ChangeDelete,
Path: "/path/to/file2",
},
}, nil
},
})
cmd := NewDiffCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetArgs([]string{"container-id"})
err := cmd.Execute()
assert.NilError(t, err)
diff := strings.SplitN(cli.OutBuffer().String(), "\n", 3)
assert.Assert(t, is.Len(diff, 3))
file0 := strings.TrimSpace(diff[0])
file1 := strings.TrimSpace(diff[1])
file2 := strings.TrimSpace(diff[2])
assert.Check(t, is.Equal(file0, "C /path/to/file0"))
assert.Check(t, is.Equal(file1, "A /path/to/file1"))
assert.Check(t, is.Equal(file2, "D /path/to/file2"))
}
func TestRunDiffClientError(t *testing.T) {
clientError := errors.New("client error")
cli := test.NewFakeCli(&fakeClient{
containerDiffFunc: func(
ctx context.Context,
containerID string,
) ([]container.FilesystemChange, error) {
return nil, clientError
},
})
cmd := NewDiffCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{"container-id"})
err := cmd.Execute()
assert.ErrorIs(t, err, clientError)
}

View File

@ -1,31 +0,0 @@
package container
import cerrdefs "github.com/containerd/errdefs"
func invalidParameter(err error) error {
if err == nil || cerrdefs.IsInvalidArgument(err) {
return err
}
return invalidParameterErr{err}
}
type invalidParameterErr struct{ error }
func (invalidParameterErr) InvalidParameter() {}
func (e invalidParameterErr) Unwrap() error {
return e.error
}
func notFound(err error) error {
if err == nil || cerrdefs.IsNotFound(err) {
return err
}
return notFoundErr{err}
}
type notFoundErr struct{ error }
func (notFoundErr) NotFound() {}
func (e notFoundErr) Unwrap() error {
return e.error
}

View File

@ -10,8 +10,9 @@ import (
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/moby/moby/api/types/container" "github.com/docker/docker/api/types"
"github.com/moby/moby/client" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -52,8 +53,8 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
options.Command = args[1:] options.Command = args[1:]
return RunExec(cmd.Context(), dockerCli, containerIDorName, options) return RunExec(cmd.Context(), dockerCli, containerIDorName, options)
}, },
ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(ctr container.Summary) bool { ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(ctr types.Container) bool {
return ctr.State != container.StatePaused return ctr.State != "paused"
}), }),
Annotations: map[string]string{ Annotations: map[string]string{
"category-top": "2", "category-top": "2",
@ -84,13 +85,13 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
} }
// RunExec executes an `exec` command // RunExec executes an `exec` command
func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName string, options ExecOptions) error { func RunExec(ctx context.Context, dockerCli command.Cli, containerIDorName string, options ExecOptions) error {
execOptions, err := parseExec(options, dockerCLI.ConfigFile()) execOptions, err := parseExec(options, dockerCli.ConfigFile())
if err != nil { if err != nil {
return err return err
} }
apiClient := dockerCLI.Client() apiClient := dockerCli.Client()
// We need to check the tty _before_ we do the ContainerExecCreate, because // We need to check the tty _before_ we do the ContainerExecCreate, because
// otherwise if we error out we will leak execIDs on the server (and // otherwise if we error out we will leak execIDs on the server (and
@ -99,13 +100,13 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin
if _, err := apiClient.ContainerInspect(ctx, containerIDorName); err != nil { if _, err := apiClient.ContainerInspect(ctx, containerIDorName); err != nil {
return err return err
} }
if !options.Detach { if !execOptions.Detach {
if err := dockerCLI.In().CheckTty(execOptions.AttachStdin, execOptions.Tty); err != nil { if err := dockerCli.In().CheckTty(execOptions.AttachStdin, execOptions.Tty); err != nil {
return err return err
} }
} }
fillConsoleSize(execOptions, dockerCLI) fillConsoleSize(execOptions, dockerCli)
response, err := apiClient.ContainerExecCreate(ctx, containerIDorName, *execOptions) response, err := apiClient.ContainerExecCreate(ctx, containerIDorName, *execOptions)
if err != nil { if err != nil {
@ -117,14 +118,14 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin
return errors.New("exec ID empty") return errors.New("exec ID empty")
} }
if options.Detach { if execOptions.Detach {
return apiClient.ContainerExecStart(ctx, execID, container.ExecStartOptions{ return apiClient.ContainerExecStart(ctx, execID, container.ExecStartOptions{
Detach: options.Detach, Detach: execOptions.Detach,
Tty: execOptions.Tty, Tty: execOptions.Tty,
ConsoleSize: execOptions.ConsoleSize, ConsoleSize: execOptions.ConsoleSize,
}) })
} }
return interactiveExec(ctx, dockerCLI, execOptions, execID) return interactiveExec(ctx, dockerCli, execOptions, execID)
} }
func fillConsoleSize(execOptions *container.ExecOptions, dockerCli command.Cli) { func fillConsoleSize(execOptions *container.ExecOptions, dockerCli command.Cli) {
@ -223,12 +224,13 @@ func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*contai
Privileged: execOpts.Privileged, Privileged: execOpts.Privileged,
Tty: execOpts.TTY, Tty: execOpts.TTY,
Cmd: execOpts.Command, Cmd: execOpts.Command,
Detach: execOpts.Detach,
WorkingDir: execOpts.Workdir, WorkingDir: execOpts.Workdir,
} }
// collect all the environment variables for the container // collect all the environment variables for the container
var err error var err error
if execOptions.Env, err = opts.ReadKVEnvStrings(execOpts.EnvFile.GetSlice(), execOpts.Env.GetSlice()); err != nil { if execOptions.Env, err = opts.ReadKVEnvStrings(execOpts.EnvFile.GetAll(), execOpts.Env.GetAll()); err != nil {
return nil, err return nil, err
} }

View File

@ -2,17 +2,17 @@ package container
import ( import (
"context" "context"
"errors"
"io" "io"
"os" "os"
"strconv"
"testing" "testing"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/moby/moby/api/types/container" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/fs" "gotest.tools/v3/fs"
@ -76,6 +76,7 @@ TWO=2
{ {
options: withDefaultOpts(ExecOptions{Detach: true}), options: withDefaultOpts(ExecOptions{Detach: true}),
expected: container.ExecOptions{ expected: container.ExecOptions{
Detach: true,
Cmd: []string{"command"}, Cmd: []string{"command"},
}, },
}, },
@ -86,6 +87,7 @@ TWO=2
Detach: true, Detach: true,
}), }),
expected: container.ExecOptions{ expected: container.ExecOptions{
Detach: true,
Tty: true, Tty: true,
Cmd: []string{"command"}, Cmd: []string{"command"},
}, },
@ -96,6 +98,7 @@ TWO=2
expected: container.ExecOptions{ expected: container.ExecOptions{
Cmd: []string{"command"}, Cmd: []string{"command"},
DetachKeys: "de", DetachKeys: "de",
Detach: true,
}, },
}, },
{ {
@ -107,6 +110,7 @@ TWO=2
expected: container.ExecOptions{ expected: container.ExecOptions{
Cmd: []string{"command"}, Cmd: []string{"command"},
DetachKeys: "ab", DetachKeys: "ab",
Detach: true,
}, },
}, },
{ {
@ -138,12 +142,10 @@ TWO=2
}, },
} }
for i, testcase := range testcases { for _, testcase := range testcases {
t.Run("test "+strconv.Itoa(i+1), func(t *testing.T) {
execConfig, err := parseExec(testcase.options, &testcase.configFile) execConfig, err := parseExec(testcase.options, &testcase.configFile)
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.DeepEqual(testcase.expected, *execConfig)) assert.Check(t, is.DeepEqual(testcase.expected, *execConfig))
})
} }
} }
@ -176,8 +178,8 @@ func TestRunExec(t *testing.T) {
doc: "inspect error", doc: "inspect error",
options: NewExecOptions(), options: NewExecOptions(),
client: &fakeClient{ client: &fakeClient{
inspectFunc: func(string) (container.InspectResponse, error) { inspectFunc: func(string) (types.ContainerJSON, error) {
return container.InspectResponse{}, errors.New("failed inspect") return types.ContainerJSON{}, errors.New("failed inspect")
}, },
}, },
expectedError: "failed inspect", expectedError: "failed inspect",
@ -206,8 +208,8 @@ func TestRunExec(t *testing.T) {
} }
} }
func execCreateWithID(_ string, _ container.ExecOptions) (container.ExecCreateResponse, error) { func execCreateWithID(_ string, _ container.ExecOptions) (types.IDResponse, error) {
return container.ExecCreateResponse{ID: "execid"}, nil return types.IDResponse{ID: "execid"}, nil
} }
func TestGetExecExitStatus(t *testing.T) { func TestGetExecExitStatus(t *testing.T) {
@ -250,14 +252,14 @@ func TestNewExecCommandErrors(t *testing.T) {
name string name string
args []string args []string
expectedError string expectedError string
containerInspectFunc func(img string) (container.InspectResponse, error) containerInspectFunc func(img string) (types.ContainerJSON, error)
}{ }{
{ {
name: "client-error", name: "client-error",
args: []string{"5cb5bb5e4a3b", "-t", "-i", "bash"}, args: []string{"5cb5bb5e4a3b", "-t", "-i", "bash"},
expectedError: "something went wrong", expectedError: "something went wrong",
containerInspectFunc: func(containerID string) (container.InspectResponse, error) { containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
return container.InspectResponse{}, errors.New("something went wrong") return types.ContainerJSON{}, errors.Errorf("something went wrong")
}, },
}, },
} }

View File

@ -7,7 +7,6 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/moby/sys/atomicwriter"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -42,28 +41,27 @@ func NewExportCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runExport(ctx context.Context, dockerCLI command.Cli, opts exportOptions) error { func runExport(ctx context.Context, dockerCli command.Cli, opts exportOptions) error {
var output io.Writer if opts.output == "" && dockerCli.Out().IsTerminal() {
if opts.output == "" {
if dockerCLI.Out().IsTerminal() {
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect") return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
} }
output = dockerCLI.Out()
} else { if err := command.ValidateOutputPath(opts.output); err != nil {
writer, err := atomicwriter.New(opts.output, 0o600)
if err != nil {
return errors.Wrap(err, "failed to export container") return errors.Wrap(err, "failed to export container")
} }
defer writer.Close()
output = writer
}
responseBody, err := dockerCLI.Client().ContainerExport(ctx, opts.container) clnt := dockerCli.Client()
responseBody, err := clnt.ContainerExport(ctx, opts.container)
if err != nil { if err != nil {
return err return err
} }
defer responseBody.Close() defer responseBody.Close()
_, err = io.Copy(output, responseBody) if opts.output == "" {
_, err := io.Copy(dockerCli.Out(), responseBody)
return err return err
} }
return command.CopyToFile(opts.output, responseBody)
}

View File

@ -42,6 +42,8 @@ func TestContainerExportOutputToIrregularFile(t *testing.T) {
cmd.SetErr(io.Discard) cmd.SetErr(io.Discard)
cmd.SetArgs([]string{"-o", "/dev/random", "container"}) cmd.SetArgs([]string{"-o", "/dev/random", "container"})
const expected = `failed to export container: cannot write to a character device file` err := cmd.Execute()
assert.Error(t, cmd.Execute(), expected) assert.Assert(t, err != nil)
expected := `"/dev/random" must be a directory or a regular file`
assert.ErrorContains(t, err, expected)
} }

View File

@ -2,7 +2,7 @@ package container
import ( import (
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/container" "github.com/docker/docker/api/types/container"
) )
const ( const (
@ -13,14 +13,7 @@ const (
) )
// NewDiffFormat returns a format for use with a diff Context // NewDiffFormat returns a format for use with a diff Context
//
// Deprecated: this function was only used internally and will be removed in the next release.
func NewDiffFormat(source string) formatter.Format { func NewDiffFormat(source string) formatter.Format {
return newDiffFormat(source)
}
// newDiffFormat returns a format for use with a diff [formatter.Context].
func newDiffFormat(source string) formatter.Format {
if source == formatter.TableFormatKey { if source == formatter.TableFormatKey {
return defaultDiffTableFormat return defaultDiffTableFormat
} }
@ -28,22 +21,16 @@ func newDiffFormat(source string) formatter.Format {
} }
// DiffFormatWrite writes formatted diff using the Context // DiffFormatWrite writes formatted diff using the Context
// func DiffFormatWrite(ctx formatter.Context, changes []container.FilesystemChange) error {
// Deprecated: this function was only used internally and will be removed in the next release. render := func(format func(subContext formatter.SubContext) error) error {
func DiffFormatWrite(fmtCtx formatter.Context, changes []container.FilesystemChange) error {
return diffFormatWrite(fmtCtx, changes)
}
// diffFormatWrite writes formatted diff using the [formatter.Context].
func diffFormatWrite(fmtCtx formatter.Context, changes []container.FilesystemChange) error {
return fmtCtx.Write(newDiffContext(), func(format func(subContext formatter.SubContext) error) error {
for _, change := range changes { for _, change := range changes {
if err := format(&diffContext{c: change}); err != nil { if err := format(&diffContext{c: change}); err != nil {
return err return err
} }
} }
return nil return nil
}) }
return ctx.Write(newDiffContext(), render)
} }
type diffContext struct { type diffContext struct {
@ -52,14 +39,12 @@ type diffContext struct {
} }
func newDiffContext() *diffContext { func newDiffContext() *diffContext {
return &diffContext{ diffCtx := diffContext{}
HeaderContext: formatter.HeaderContext{ diffCtx.Header = formatter.SubHeaderContext{
Header: formatter.SubHeaderContext{
"Type": changeTypeHeader, "Type": changeTypeHeader,
"Path": pathHeader, "Path": pathHeader,
},
},
} }
return &diffCtx
} }
func (d *diffContext) MarshalJSON() ([]byte, error) { func (d *diffContext) MarshalJSON() ([]byte, error) {

View File

@ -5,7 +5,7 @@ import (
"testing" "testing"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/container" "github.com/docker/docker/api/types/container"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
) )
@ -16,7 +16,7 @@ func TestDiffContextFormatWrite(t *testing.T) {
expected string expected string
}{ }{
{ {
formatter.Context{Format: newDiffFormat("table")}, formatter.Context{Format: NewDiffFormat("table")},
`CHANGE TYPE PATH `CHANGE TYPE PATH
C /var/log/app.log C /var/log/app.log
A /usr/app/app.js A /usr/app/app.js
@ -24,7 +24,7 @@ D /usr/app/old_app.js
`, `,
}, },
{ {
formatter.Context{Format: newDiffFormat("table {{.Path}}")}, formatter.Context{Format: NewDiffFormat("table {{.Path}}")},
`PATH `PATH
/var/log/app.log /var/log/app.log
/usr/app/app.js /usr/app/app.js
@ -32,7 +32,7 @@ D /usr/app/old_app.js
`, `,
}, },
{ {
formatter.Context{Format: newDiffFormat("{{.Type}}: {{.Path}}")}, formatter.Context{Format: NewDiffFormat("{{.Type}}: {{.Path}}")},
`C: /var/log/app.log `C: /var/log/app.log
A: /usr/app/app.js A: /usr/app/app.js
D: /usr/app/old_app.js D: /usr/app/old_app.js
@ -47,10 +47,11 @@ D: /usr/app/old_app.js
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
tc.context.Output = out tc.context.Output = out
err := diffFormatWrite(tc.context, diffs) err := DiffFormatWrite(tc.context, diffs)
if err != nil { if err != nil {
assert.Error(t, err, tc.expected) assert.Error(t, err, tc.expected)
} else { } else {

View File

@ -5,7 +5,8 @@ import (
"sync" "sync"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/docker/go-units" "github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units"
) )
const ( const (
@ -21,8 +22,6 @@ const (
winMemUseHeader = "PRIV WORKING SET" // Used only on Windows winMemUseHeader = "PRIV WORKING SET" // Used only on Windows
memUseHeader = "MEM USAGE / LIMIT" // Used only on Linux memUseHeader = "MEM USAGE / LIMIT" // Used only on Linux
pidsHeader = "PIDS" // Used only on Linux pidsHeader = "PIDS" // Used only on Linux
noValue = "--"
) )
// StatsEntry represents the statistics data collected from a container // StatsEntry represents the statistics data collected from a container
@ -170,19 +169,19 @@ func (c *statsContext) Name() string {
if len(c.s.Name) > 1 { if len(c.s.Name) > 1 {
return c.s.Name[1:] return c.s.Name[1:]
} }
return noValue return "--"
} }
func (c *statsContext) ID() string { func (c *statsContext) ID() string {
if c.trunc { if c.trunc {
return formatter.TruncateID(c.s.ID) return stringid.TruncateID(c.s.ID)
} }
return c.s.ID return c.s.ID
} }
func (c *statsContext) CPUPerc() string { func (c *statsContext) CPUPerc() string {
if c.s.IsInvalid { if c.s.IsInvalid {
return noValue return "--"
} }
return formatPercentage(c.s.CPUPercentage) return formatPercentage(c.s.CPUPercentage)
} }
@ -199,28 +198,28 @@ func (c *statsContext) MemUsage() string {
func (c *statsContext) MemPerc() string { func (c *statsContext) MemPerc() string {
if c.s.IsInvalid || c.os == winOSType { if c.s.IsInvalid || c.os == winOSType {
return noValue return "--"
} }
return formatPercentage(c.s.MemoryPercentage) return formatPercentage(c.s.MemoryPercentage)
} }
func (c *statsContext) NetIO() string { func (c *statsContext) NetIO() string {
if c.s.IsInvalid { if c.s.IsInvalid {
return noValue return "--"
} }
return units.HumanSizeWithPrecision(c.s.NetworkRx, 3) + " / " + units.HumanSizeWithPrecision(c.s.NetworkTx, 3) return units.HumanSizeWithPrecision(c.s.NetworkRx, 3) + " / " + units.HumanSizeWithPrecision(c.s.NetworkTx, 3)
} }
func (c *statsContext) BlockIO() string { func (c *statsContext) BlockIO() string {
if c.s.IsInvalid { if c.s.IsInvalid {
return noValue return "--"
} }
return units.HumanSizeWithPrecision(c.s.BlockRead, 3) + " / " + units.HumanSizeWithPrecision(c.s.BlockWrite, 3) return units.HumanSizeWithPrecision(c.s.BlockRead, 3) + " / " + units.HumanSizeWithPrecision(c.s.BlockWrite, 3)
} }
func (c *statsContext) PIDs() string { func (c *statsContext) PIDs() string {
if c.s.IsInvalid || c.os == winOSType { if c.s.IsInvalid || c.os == winOSType {
return noValue return "--"
} }
return strconv.FormatUint(c.s.PidsCurrent, 10) return strconv.FormatUint(c.s.PidsCurrent, 10)
} }

View File

@ -5,13 +5,13 @@ import (
"testing" "testing"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/test" "github.com/docker/docker/pkg/stringid"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
) )
func TestContainerStatsContext(t *testing.T) { func TestContainerStatsContext(t *testing.T) {
containerID := test.RandomID() containerID := stringid.GenerateRandomID()
var ctx statsContext var ctx statsContext
tt := []struct { tt := []struct {
@ -148,7 +148,7 @@ container2 -- --
`, `,
}, },
} }
entries := []StatsEntry{ stats := []StatsEntry{
{ {
Container: "container1", Container: "container1",
CPUPercentage: 20, CPUPercentage: 20,
@ -178,10 +178,11 @@ container2 -- --
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out
err := statsFormatWrite(tc.context, entries, "windows", false) err := statsFormatWrite(tc.context, stats, "windows", false)
if err != nil { if err != nil {
assert.Error(t, err, tc.expected) assert.Error(t, err, tc.expected)
} else { } else {
@ -222,6 +223,7 @@ func TestContainerStatsContextWriteWithNoStats(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
err := statsFormatWrite(tc.context, []StatsEntry{}, "linux", false) err := statsFormatWrite(tc.context, []StatsEntry{}, "linux", false)
assert.NilError(t, err) assert.NilError(t, err)
@ -263,6 +265,7 @@ func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
err := statsFormatWrite(tc.context, []StatsEntry{}, "windows", false) err := statsFormatWrite(tc.context, []StatsEntry{}, "windows", false)
assert.NilError(t, err) assert.NilError(t, err)
@ -273,46 +276,45 @@ func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) {
} }
func TestContainerStatsContextWriteTrunc(t *testing.T) { func TestContainerStatsContextWriteTrunc(t *testing.T) {
tests := []struct { var out bytes.Buffer
doc string
contexts := []struct {
context formatter.Context context formatter.Context
trunc bool trunc bool
expected string expected string
}{ }{
{ {
doc: "non-truncated", formatter.Context{
context: formatter.Context{
Format: "{{.ID}}", Format: "{{.ID}}",
Output: &out,
}, },
expected: "b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc\n", false,
"b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc\n",
}, },
{ {
doc: "truncated", formatter.Context{
context: formatter.Context{
Format: "{{.ID}}", Format: "{{.ID}}",
Output: &out,
}, },
trunc: true, true,
expected: "b95a83497c91\n", "b95a83497c91\n",
}, },
} }
for _, tc := range tests { for _, context := range contexts {
t.Run(tc.doc, func(t *testing.T) { statsFormatWrite(context.context, []StatsEntry{{ID: "b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc"}}, "linux", context.trunc)
var out bytes.Buffer assert.Check(t, is.Equal(context.expected, out.String()))
tc.context.Output = &out // Clean buffer
err := statsFormatWrite(tc.context, []StatsEntry{{ID: "b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc"}}, "linux", tc.trunc) out.Reset()
assert.NilError(t, err)
assert.Check(t, is.Equal(tc.expected, out.String()))
})
} }
} }
func BenchmarkStatsFormat(b *testing.B) { func BenchmarkStatsFormat(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
entries := genStats() stats := genStats()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
for _, s := range entries { for _, s := range stats {
_ = s.CPUPerc() _ = s.CPUPerc()
_ = s.MemUsage() _ = s.MemUsage()
_ = s.MemPerc() _ = s.MemPerc()
@ -335,9 +337,9 @@ func genStats() []statsContext {
NetworkTx: 987.654321, NetworkTx: 987.654321,
PidsCurrent: 123456789, PidsCurrent: 123456789,
}} }}
entries := make([]statsContext, 0, 100) stats := make([]statsContext, 100)
for range 100 { for i := 0; i < 100; i++ {
entries = append(entries, entry) stats = append(stats, entry)
} }
return entries return stats
} }

View File

@ -8,8 +8,9 @@ import (
"sync" "sync"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/moby/moby/api/stdcopy" "github.com/docker/docker/api/types"
"github.com/moby/moby/api/types" "github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/stdcopy"
"github.com/moby/term" "github.com/moby/term"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -18,18 +19,6 @@ import (
// TODO: This could be moved to `pkg/term`. // TODO: This could be moved to `pkg/term`.
var defaultEscapeKeys = []byte{16, 17} var defaultEscapeKeys = []byte{16, 17}
// readCloserWrapper wraps an io.Reader, and implements an io.ReadCloser
// It calls the given callback function when closed.
type readCloserWrapper struct {
io.Reader
closer func() error
}
// Close calls back the passed closer function
func (r *readCloserWrapper) Close() error {
return r.closer()
}
// A hijackedIOStreamer handles copying input to and output from streams to the // A hijackedIOStreamer handles copying input to and output from streams to the
// connection. // connection.
type hijackedIOStreamer struct { type hijackedIOStreamer struct {
@ -95,9 +84,12 @@ func (h *hijackedIOStreamer) setupInput() (restore func(), err error) {
// Use sync.Once so we may call restore multiple times but ensure we // Use sync.Once so we may call restore multiple times but ensure we
// only restore the terminal once. // only restore the terminal once.
restore = sync.OnceFunc(func() { var restoreOnce sync.Once
restore = func() {
restoreOnce.Do(func() {
_ = restoreTerminal(h.streams, h.inputStream) _ = restoreTerminal(h.streams, h.inputStream)
}) })
}
// Wrap the input to detect detach escape sequence. // Wrap the input to detect detach escape sequence.
// Use default escape keys if an invalid sequence is given. // Use default escape keys if an invalid sequence is given.
@ -111,10 +103,7 @@ func (h *hijackedIOStreamer) setupInput() (restore func(), err error) {
} }
} }
h.inputStream = &readCloserWrapper{ h.inputStream = ioutils.NewReadCloserWrapper(term.NewEscapeProxy(h.inputStream, escapeKeys), h.inputStream.Close)
Reader: term.NewEscapeProxy(h.inputStream, escapeKeys),
closer: h.inputStream.Close,
}
return restore, nil return restore, nil
} }

View File

@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.23 //go:build go1.21
package container package container
@ -42,9 +42,11 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error {
apiClient := dockerCLI.Client() client := dockerCli.Client()
return inspect.Inspect(dockerCLI.Out(), opts.refs, opts.format, func(ref string) (any, []byte, error) {
return apiClient.ContainerInspectWithRaw(ctx, ref, opts.size) getRefFunc := func(ref string) (any, []byte, error) {
}) return client.ContainerInspectWithRaw(ctx, ref, opts.size)
}
return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRefFunc)
} }

View File

@ -2,12 +2,13 @@ package container
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -43,19 +44,20 @@ func NewKillCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runKill(ctx context.Context, dockerCLI command.Cli, opts *killOptions) error { func runKill(ctx context.Context, dockerCli command.Cli, opts *killOptions) error {
apiClient := dockerCLI.Client() var errs []string
errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error { errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error {
return apiClient.ContainerKill(ctx, container, opts.signal) return dockerCli.Client().ContainerKill(ctx, container, opts.signal)
}) })
var errs []error
for _, name := range opts.containers { for _, name := range opts.containers {
if err := <-errChan; err != nil { if err := <-errChan; err != nil {
errs = append(errs, err) errs = append(errs, err.Error())
continue } else {
_, _ = fmt.Fprintln(dockerCli.Out(), name)
} }
_, _ = fmt.Fprintln(dockerCLI.Out(), name)
} }
return errors.Join(errs...) if len(errs) > 0 {
return errors.New(strings.Join(errs, "\n"))
}
return nil
} }

View File

@ -1,74 +0,0 @@
package container
import (
"context"
"fmt"
"io"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestRunKill(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerKillFunc: func(
ctx context.Context,
container string,
signal string,
) error {
assert.Assert(t, is.Equal(signal, "STOP"))
return nil
},
})
cmd := NewKillCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetArgs([]string{
"--signal", "STOP",
"container-id-1",
"container-id-2",
})
err := cmd.Execute()
assert.NilError(t, err)
containerIDs := strings.SplitN(cli.OutBuffer().String(), "\n", 2)
assert.Assert(t, is.Len(containerIDs, 2))
containerID1 := strings.TrimSpace(containerIDs[0])
containerID2 := strings.TrimSpace(containerIDs[1])
assert.Check(t, is.Equal(containerID1, "container-id-1"))
assert.Check(t, is.Equal(containerID2, "container-id-2"))
}
func TestRunKillClientError(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerKillFunc: func(
ctx context.Context,
container string,
signal string,
) error {
return fmt.Errorf("client error for container %s", container)
},
})
cmd := NewKillCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{"container-id-1", "container-id-2"})
err := cmd.Execute()
errs := strings.SplitN(err.Error(), "\n", 2)
assert.Assert(t, is.Len(errs, 2))
errContainerID1 := errs[0]
errContainerID2 := errs[1]
assert.Assert(t, is.Equal(errContainerID1, "client error for container container-id-1"))
assert.Assert(t, is.Equal(errContainerID2, "client error for container container-id-2"))
}

View File

@ -11,7 +11,7 @@ import (
flagsHelper "github.com/docker/cli/cli/flags" flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/docker/cli/templates" "github.com/docker/cli/templates"
"github.com/moby/moby/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )

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