Compare commits

...

114 Commits

Author SHA1 Message Date
952c807716 Merge pull request #4926 from vvoland/vendor-docker
Some checks failed
build / prepare-plugins (push) Has been cancelled
build / plugins (push) Has been cancelled
codeql / codeql (push) Has been cancelled
e2e / e2e (alpine, 23, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 23, experimental) (push) Has been cancelled
e2e / e2e (alpine, 23, non-experimental) (push) Has been cancelled
e2e / e2e (alpine, 24, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 24, experimental) (push) Has been cancelled
e2e / e2e (alpine, 24, non-experimental) (push) Has been cancelled
e2e / e2e (alpine, 25, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 25, experimental) (push) Has been cancelled
e2e / e2e (alpine, 25, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 23, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 23, experimental) (push) Has been cancelled
e2e / e2e (debian, 23, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 24, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 24, experimental) (push) Has been cancelled
e2e / e2e (debian, 24, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 25, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 25, experimental) (push) Has been cancelled
e2e / e2e (debian, 25, non-experimental) (push) Has been cancelled
test / ctn (push) Has been cancelled
test / host (macos-12) (push) Has been cancelled
validate / validate (lint) (push) Has been cancelled
validate / validate (shellcheck) (push) Has been cancelled
validate / validate (update-authors) (push) Has been cancelled
validate / validate (validate-vendor) (push) Has been cancelled
validate / validate-md (push) Has been cancelled
validate / validate-make (manpages) (push) Has been cancelled
validate / validate-make (yamldocs) (push) Has been cancelled
vendor: github.com/docker/docker f4c696eef17d62a42
2024-03-07 19:35:08 +01:00
a8379092af vendor: github.com/docker/docker f4c696eef17d62a42
full diff: https://github.com/docker/docker/compare/v26.0.0-rc1+incompatible...f4c696eef17d62a421877d95c4810185750c5641

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-07 19:13:04 +01:00
a1361a1372 Merge pull request #4923 from vvoland/cli-arm64
bake: Add `windows/arm64` target to bin-image-cross
2024-03-06 09:08:20 -07:00
ab9d560570 bake: Add windows/arm64 target to bin-image-cross
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-06 16:46:39 +01:00
ba6460829b Merge pull request #4918 from vvoland/go-1.21.8
update to go1.21.8
2024-03-05 22:24:30 +01:00
3b77477943 update to go1.21.8
go1.21.8 (released 2024-03-05) includes 5 security fixes:

- crypto/x509: Verify panics on certificates with an unknown public key algorithm (CVE-2024-24783, https://go.dev/issue/65390)
- net/http: memory exhaustion in Request.ParseMultipartForm (CVE-2023-45290, https://go.dev/issue/65383)
- net/http, net/http/cookiejar: incorrect forwarding of sensitive headers and cookies on HTTP redirect (CVE-2023-45289, https://go.dev/issue/65065)
- html/template: errors returned from MarshalJSON methods may break template escaping (CVE-2024-24785, https://go.dev/issue/65697)
- net/mail: comments in display names are incorrectly handled (CVE-2024-24784, https://go.dev/issue/65083)

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

- https://github.com/golang/go/issues?q=milestone%3AGo1.21.8+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.21.6...go1.21.8

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-05 22:08:26 +01:00
181575bf55 Merge pull request #4888 from Benehiko/fix-prompt-termination
fix: cli prompt termination exit code
2024-03-04 07:56:38 -07:00
10bf91a02d fix: cli prompt termination exit code
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
2024-03-04 15:26:17 +01:00
310daf2db9 Merge pull request #4259 from gmargaritis/373-support-detach-flag-in-stack-rm
Add support for --detach flag in stack rm
2024-03-04 10:04:41 +01:00
238d659ff9 Add support for --detach/-d flag in stack rm
Added --detach/-d to stack rm. Setting --detach=false waits until
all of the stack tasks have reached a terminal state.

Co-authored-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: George Margaritis <gmargaritis@protonmail.com>
2024-03-04 09:56:53 +01:00
4a43b8eaed Merge pull request #4258 from gmargaritis/373-support-detach-in-docker-stack-deploy
Add support for --detach flag in stack deploy
2024-03-04 09:45:01 +01:00
b086d72769 Add support for --detach flag in stack deploy
Added --detach and --quiet/-q flags to stack deploy. Setting --detach=false
waits until all of the stack services have converged. Shows progress bars for
each individual task, unless  --quiet/-q is specified.

Co-authored-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: George Margaritis <gmargaritis@protonmail.com>
2024-03-04 09:36:36 +01:00
35e6a41ff2 Merge pull request #4906 from Benehiko/benehiko/v26.0.0-rc1
vendor: github.com/docker/docker v26.0.0-rc1
2024-03-01 18:33:58 +01:00
5e80232398 vendor: github.com/docker/docker v26.0.0-rc1
full diff: https://github.com/docker/docker/compare/c70d7905fbd9...v26.0.0-rc1

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>

test: update fixtures

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
2024-03-01 18:23:25 +01:00
19d02cd101 Merge pull request #4896 from Benehiko/help-plugin-descriptions
chore: `docker help` should not show plugin vendor and version
2024-02-29 07:32:20 -07:00
ec35bab4fa Merge pull request #4901 from dvdksn/docs-fix-typo-tty
docs: fix typo in tty example
2024-02-29 14:53:02 +01:00
d51ba41791 docs: prefer -it over -ti
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-02-29 13:38:53 +01:00
a5b82e9f87 docs: fix typo in tty example
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-02-29 13:38:48 +01:00
abe78b79de chore: docker help should not show plugin vendor and version
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
2024-02-29 08:47:18 +01:00
9015b71163 Merge pull request #4875 from jsternberg/build-alias-env-var
plugins: set OTEL_RESOURCE_ATTRIBUTES when invoking a plugin
2024-02-28 22:02:22 +01:00
5786f20687 plugins: fix encoding for OTEL env var passed to plugin
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2024-02-28 12:43:07 -08:00
85dcacd78f plugins: set OTEL_RESOURCE_ATTRIBUTES when invoking a plugin
When a plugin is invoked, the docker cli will now set
`OTEL_RESOURCE_ATTRIBUTES` to pass OTEL resource attribute names to the
plugin as additional resource attributes. At the moment, the only
resource attribute passed is `cobra.command_path`.

All resource attributes passed by the CLI are prepended with the
namespace `docker.cli` to avoid clashing with existing ones the plugin
uses or ones defined by the user.

For aliased commands like the various builder commands, the command path
is overwritten to match with the original name (such as `docker
builder`) instead of the forwarded name (such as `docker buildx build`).

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
2024-02-28 12:43:05 -08:00
eb306df13d Merge pull request #4887 from dvdksn/invalid-daemon.json
Some checks failed
build / prepare-plugins (push) Has been cancelled
build / plugins (push) Has been cancelled
codeql / codeql (push) Has been cancelled
e2e / e2e (alpine, 23, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 23, experimental) (push) Has been cancelled
e2e / e2e (alpine, 23, non-experimental) (push) Has been cancelled
e2e / e2e (alpine, 24, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 24, experimental) (push) Has been cancelled
e2e / e2e (alpine, 24, non-experimental) (push) Has been cancelled
e2e / e2e (alpine, 25, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 25, experimental) (push) Has been cancelled
e2e / e2e (alpine, 25, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 23, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 23, experimental) (push) Has been cancelled
e2e / e2e (debian, 23, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 24, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 24, experimental) (push) Has been cancelled
e2e / e2e (debian, 24, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 25, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 25, experimental) (push) Has been cancelled
e2e / e2e (debian, 25, non-experimental) (push) Has been cancelled
test / ctn (push) Has been cancelled
test / host (macos-12) (push) Has been cancelled
validate / validate (lint) (push) Has been cancelled
validate / validate (shellcheck) (push) Has been cancelled
validate / validate (update-authors) (push) Has been cancelled
validate / validate (validate-vendor) (push) Has been cancelled
validate / validate-md (push) Has been cancelled
validate / validate-make (manpages) (push) Has been cancelled
validate / validate-make (yamldocs) (push) Has been cancelled
docs: fix invalid daemon.json examples
2024-02-28 18:00:17 +01:00
efb9206433 Merge pull request #4897 from thaJeztah/bump_engine
vendor: github.com/docker/docker c70d7905fbd9 (v26.0.0-dev)
2024-02-26 19:24:28 +01:00
acc675014f vendor: github.com/docker/docker c70d7905fbd9 (v26.0.0-dev)
full diff: 86b86412a1...c70d7905fb

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-26 17:54:20 +01:00
79541b7e21 vendor: google.golang.org/grpc v1.59.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-26 17:53:12 +01:00
096ced0894 vendor: OTEL v0.46.1 / v1.21.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-26 17:50:23 +01:00
f3c77df31e vendor: github.com/prometheus/client_golang v1.17.0
full diffs:

- https://github.com/prometheus/client_golang/compare/v1.14.0...v1.17.0
- https://github.com/prometheus/client_model/compare/v0.3.0...v0.5.0
- https://github.com/prometheus/common/compare/v0.42.0...v0.44.0
- https://github.com/prometheus/procfs/compare/v0.9.0...v0.12.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-26 17:45:07 +01:00
1b42d04d63 vendor: github.com/go-logr/logr v1.3.0
full diff: https://github.com/go-logr/logr/compare/v1.2.4...v1.3.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-26 17:39:47 +01:00
f5a29ff8eb vendor: github.com/containerd/containerd v1.7.13
no changes in vendored files

full diff: https://github.com/containerd/containerd/compare/v1.7.12...v1.7.13

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-26 17:36:46 +01:00
dc6bfac81a Merge pull request #4331 from vvoland/mount-volsubpath
mount: Add volume-subpath option
2024-02-23 17:29:42 +01:00
9767d02493 Merge pull request #4894 from crazy-max/codecov-token
ci: set codecov token
2024-02-22 23:20:49 +01:00
2663c10574 ci: set codecov token
Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
2024-02-22 18:11:18 +01:00
48a670f26b docs: fix invalid daemon.json examples
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-02-21 12:29:31 +01:00
1443014cbd Merge pull request #4884 from thaJeztah/regenerate_mdddocs
docs: regenerate markdown
2024-02-21 11:33:23 +01:00
f2e98f9a93 docs: regenerate markdown
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-21 10:40:13 +01:00
f6b7a11b93 Merge pull request #4866 from dvdksn/docs-cli-reference-urlscheme
docs: update link targets
2024-02-21 09:18:52 +01:00
caf72655fb docs: update url scheme for reference docs
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-02-20 21:38:50 +01:00
e244044944 deps: update cli-docs-tool version (v0.7.0)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-02-20 08:31:53 +01:00
a10debe6c4 Merge pull request #4881 from thaJeztah/update_go_1.21.7
update to go1.21.7
2024-02-19 15:18:52 +01:00
20b9d489e0 update to go1.21.7
go1.21.7 (released 2024-02-06) includes fixes to the compiler, the go command,
the runtime, and the crypto/x509 package. See the Go 1.21.7 milestone on our
issue tracker for details:

- https://github.com/golang/go/issues?q=milestone%3AGo1.21.7+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.21.6...go1.21.7

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-19 14:13:51 +01:00
67105e0995 Merge pull request #4876 from systemmonkey42/patch-1
Fixed typo in bash completion functions
2024-02-19 11:50:49 +01:00
c514003e69 Fixed typo in bash completion functions
Signed-off-by: David le Blanc <david-le-blanc@users.noreply.github.com>
2024-02-17 17:11:28 +11:00
49a44fb73f Merge pull request #4879 from qyanu-pull-requests/master
man/docker-build.1: typo in parameter variable: CID => IID
2024-02-16 18:27:24 +01:00
ff5b0d18fe man/docker-build.1: typo in parameter variable: CID => IID
--iidfile logically specifies the IIDFILE and not the CIDFILE
(use --cidfile from docker-run.1.md for specifying a cidfile).

Signed-off-by: Max-Julian Pogner <max-julian@pogner.at>
2024-02-16 11:10:06 +01:00
21d96ffb8f Merge pull request #4839 from krissetto/upgrade-ci
update CI
2024-02-15 14:09:42 +01:00
155b5b4eaa Merge pull request #4872 from thaJeztah/use_non_deprecated_uuid
cli-plugins/socket: remove use of deprecated distribution uuid package
2024-02-14 10:33:46 +01:00
3b5e814242 cli-plugins/socket: remove use of deprecated distribution uuid package
The "github.com/docker/distribution" module moved to the distribution
org ("github.com/docker/distribution/v3"), and the new module deprecated
and removed the uuid package in favor of Google's UUID package.

While we still depend on the old module through packages and as an indirect
dependency, we may want to try avoid using it.

This patch replaces the use for the socket package, and replaces it for a
local utility, taking the same approach as `stringid.GenerateRandomID()`,
which should be random enough for this purpose.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-13 16:24:53 +01:00
69ed6588a8 Cleanup of dockerfiles, compose files and env vars
Signed-off-by: Christopher Petito <chrisjpetito@gmail.com>
2024-02-13 13:32:45 +00:00
6b67b95493 Update gha runners and engines used in e2e tests
- gha runners updated to ubuntu 22.04
- e2e now runs against moby 23.0, 24.0 and 25.0
- temporarily skip broken test for moby < 25

Signed-off-by: Christopher Petito <chrisjpetito@gmail.com>
2024-02-13 13:30:09 +00:00
edc09e6038 mount: Add volume-subpath option
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-02-13 14:04:00 +01:00
a253318869 Merge pull request #4873 from vvoland/test-daemon-apiversion
testenv: Add DaemonAPIVersion helper
2024-02-13 13:50:35 +01:00
9831fea4db testenv: Add DaemonAPIVersion helper
Allow tests to check the negotiated API version used by the client.

Can be used to skip tests based on API versions, for example:
```go
    skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.44"))
```

will skip the test if the API version is older than 1.44

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-02-12 16:29:47 +01:00
d1b88930c3 Merge pull request #4867 from krissetto/test-fixes
Test fixes needed for upgrading ci runners and engine
2024-02-12 14:40:22 +01:00
d7f195833d Merge pull request #4862 from catap/openbsd
Avoid keeping @docker_cli_[UUID] files
2024-02-12 13:53:33 +01:00
b807ea8084 Merge pull request #4868 from thaJeztah/update_engine
vendor: github.com/docker/docker 86b86412a1b7 (v26.0-dev)
2024-02-12 09:36:04 +01:00
1cda2c45f8 Merge pull request #4869 from thaJeztah/commandcon_cleanup
cli/connhelper/commandconn: inline variables
2024-02-12 07:46:14 +01:00
8bae662713 cli/connhelper/commandconn: inline variables
Inline the variables used to define the command + args used in the
tests, which makes it slightly easier to see what's run.

Also explicitly define a context, in case we want to add telemetry
to these tests.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-10 13:59:36 +01:00
df6220d434 vendor: github.com/docker/docker 86b86412a1b7 (v26.0-dev)
full diff: 9e075f3808...86b86412a1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-10 12:26:39 +01:00
30dd7c1319 Minor test fixes necessary for eventually upgrading ci runners and engine version
Signed-off-by: Christopher Petito <chrisjpetito@gmail.com>
2024-02-09 13:55:03 +00:00
2c214241fa Avoid keeping @docker_cli_[UUID] files
Seems that OpenBSD behaves like darwin and requires to unlink all
socket, after it was used.

Tested on OpenBSD 7.4

Signed-off-by: Kirill A. Korinsky <kirill@korins.ky>
2024-02-09 00:19:27 +01:00
34797d1678 Merge pull request #4860 from robmry/4859_compose_extra_hosts_eq_sep
docker stack: allow '=' separator in extra_hosts
2024-02-08 17:08:47 +01:00
c986d09bca docker stack: allow '=' separator in extra_hosts
extra_hosts in the compose file format allows '=' as a separator, and brackets
around IP addresses, the engine API doesn't.

So, transform the values when reading a compose file for 'docker stack'.

Signed-off-by: Rob Murray <rob.murray@docker.com>
2024-02-07 17:57:17 +00:00
79fa65e7b5 Merge pull request #4835 from thaJeztah/update_engine_api_removal
vendor: github.com/docker/docker 9e075f3808a5 (master, v26.0.0-dev)
2024-02-07 10:47:21 +01:00
324309b086 vendor: github.com/docker/docker 9e075f3808a5 (master, v26.0.0-dev)
Vendor docker/docker with API < 1.24 removed. This should not affect client
code.

43ffb1ee9d..9e075f3808

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-07 02:27:53 +01:00
93ad9fbdf6 vendor: github.com/moby/swarmkit/v2 v2.0.0-20240125134710-dcda100a8261
full diff: f082dd7a0c...dcda100a82

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-07 02:23:51 +01:00
57d72237d7 Merge pull request #4854 from krissetto/uniform-volume-prune-output
Uniform output of volume prune cmd with other prune cmds
2024-02-06 19:45:51 +01:00
69e0f53a03 Uniform volume prune output msg with other prune commands
- Return error when user refuses at confirmation prompt
- Avoid sending space freed msg if user cancelled
- Fixed unit tests

Signed-off-by: Christopher Petito <chrisjpetito@gmail.com>
2024-02-06 14:04:53 +00:00
ce3b07c0db Merge pull request #4849 from thaJeztah/image_list_dedup
images: print hint when invoking "docker images" with ambiguous argument
2024-02-05 16:25:51 +01:00
809eb8cdee images: print hint when invoking "docker images" with ambiguous argument
The `docker images` top-level subcommand predates the `docker <object> <verb>`
convention (e.g. `docker image ls`), but accepts a positional argument to
search/filter images by name (globbing). It's common for users to accidentally
mistake these commands, and to use (e.g.) `docker images ls`, expecting
to see all images, but ending up with an empty list because no image named
"ls" was found.

Disallowing these search-terms would be a breaking change, but we can print
and informational message to help the users correct their mistake.

Before this patch:

    docker images ls
    REPOSITORY   TAG       IMAGE ID   CREATED   SIZE

With this patch applied:

    docker images ls
    REPOSITORY   TAG       IMAGE ID   CREATED   SIZE

    No images found matching "ls": did you mean "docker image ls"?

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-03 18:10:55 +01:00
b158181a1d cli/command/images: runImages: use proper camel-case for dockerCLI
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-03 17:56:06 +01:00
1328bb3381 cli/command/images: runImages: inline intermediate var
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-03 17:54:23 +01:00
843951e84a Merge pull request #4843 from docker/dependabot/github_actions/codecov/codecov-action-4
build(deps): Bump codecov/codecov-action from 3 to 4
2024-02-01 15:47:14 +01:00
b123ce6526 build(deps): Bump codecov/codecov-action from 3 to 4
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-01 08:57:32 +00:00
297704984b Merge pull request #4396 from zliang-akamai/patch-1
Add Linode docker volume plugin
2024-01-31 12:55:31 +01:00
d001ac4892 Merge pull request #4836 from thaJeztah/remove_dummy_circleci
Remove dummy circleci config
2024-01-30 17:46:03 +01:00
1587b70ee7 Remove dummy circleci config
The 20.10 branch reached EOL, so we can probably remove this
one again.

This reverts commit a3d53e319f.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-30 17:29:11 +01:00
3865da2bc8 Merge pull request #4828 from laurazard/plugin-socket-tests
Add tests for CLI/plugin communication (i.e. let's not break everyone's plugins 🥲)
2024-01-29 14:46:25 +01:00
cfa9fef77d tests: add plugin-socket-compatibility tests
Adds a new plugin to the e2e plugins that simulates an older
plugin binary and a test suite to ensure older plugin binaries
keep behaving the same with newer CLI versions.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-01-29 13:39:58 +00:00
6dcf285ff1 Merge pull request #4834 from thaJeztah/flags_dont_varp
cli/command: don't use pflags.XXXVarP for flags without shorthand
2024-01-29 12:26:08 +01:00
dfdff11a22 cli/command: don't use pflags.XXXVarP for flags without shorthand
Use the `XXXVar` equivalent for flags that don't have a shorthand flag
instead of passing an empty string for the shorthand flag.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-29 11:16:38 +01:00
abf8cff233 Merge pull request #4832 from thaJeztah/docker_attach_fixups
cli/command/container: minor cleanup in attach
2024-01-26 16:45:59 +01:00
0ef5e95481 Merge pull request #4831 from thaJeztah/bump_compose
Dockerfile: update docker compose to v2.24.3
2024-01-26 16:19:38 +01:00
690f63e6d3 cli/command/container: minor cleanup in attach
- rename confusing `target` argument, and use `containerID` in all places;
  also make the variable more clearly local-scoped.
- rename `dockerCli` to be correctly camel-case, and to be consistent in
  all places in this file.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-26 13:38:09 +01:00
53e2e54c29 Dockerfile: update docker compose to v2.24.3
Update the version of compose used in CI to the latest version.

- full diff: https://github.com/docker/compose/compare/v2.24.2...v2.24.3
- release notes: https://github.com/docker/compose/releases/tag/v2.24.2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-26 11:06:44 +01:00
1c4d6d85dd scripts: don't hardcode architecture in e2e script
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-01-25 15:37:17 +00:00
a1bd689a4d Merge pull request #4821 from vvoland/vendor-v26.0.0-43ffb1ee9d5a
vendor: github.com/docker/docker 43ffb1ee9d5a (v26.0.0-dev)
2024-01-24 15:06:06 +01:00
4fa2fe9b9e Replace deprecated types.Image* usage
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-01-24 14:59:10 +01:00
68dac842a1 vendor: github.com/docker/docker 43ffb1ee9d5a (v26.0.0-dev)
full diff: https://github.com/docker/docker/compare/v25.0.0...43ffb1ee9d5a

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-01-24 14:59:08 +01:00
bb1cb4992c Merge pull request #4820 from thaJeztah/bump_engine_25.0.1
vendor: github.com/docker/docker v25.0.1
2024-01-24 14:57:12 +01:00
9e2615bc46 Merge pull request #4809 from thaJeztah/remove_printSecurityOptionsWarnings
info: remove printSecurityOptionsWarnings, printServerWarnings
2024-01-24 12:36:19 +01:00
4b1ed1f442 vendor: github.com/docker/docker v25.0.1
relevant changes:

- Fix isGitURL regular expression
- pkg/system: return even richer xattr errors

full diff: https://github.com/moby/moby/compare/v25.0.0...v25.0.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-24 12:17:20 +01:00
d9294f06b5 info: remove printSecurityOptionsWarnings, printServerWarnings
Docker Engine 1.13 (API v1.25) added an option to set a custom default seccomp
profile on the daemon (see [moby/moby@b237189]). A warning was added on the
client-side if a non-default profile was set.

Docker Engine 23.0 (API v1.42) added warnings about non-default seccomp
profiles to the "info" response ([moby/moby@04f932a]), and the client was
updated to skip generating client-side warnings for API v1.42 and up in
[docker/cli@8964595].

These warnings are purely informational, and given that Docker Engine versions
before 23.0 have reached EOL, and any current version of the Engine now returns
the Warnings, it should be safe to remove the client-side fall back logic.

This patch removes the client-side fall back code for warnings that was
added in 8964595692.

[moby/moby@b237189]: b237189e6c
[moby/moby@04f932a]: 04f932ac86
[docker/cli@8964595]: 8964595692

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-23 18:23:35 +01:00
c6ae74956c Merge pull request #4808 from thaJeztah/remove_printServerWarningsLegacy
info: remove printServerWarningsLegacy
2024-01-23 18:22:58 +01:00
69a4fcc3bb Merge pull request #4810 from thaJeztah/remove_deprecated_cliopts
cli/command: remove deprecated DockerCliOption, InitializeOpt
2024-01-23 18:22:28 +01:00
ea84a3fc31 Merge pull request #4811 from thaJeztah/remove_deprecated_NewStartOptions
cli/command/container: remove deprecated NewStartOptions
2024-01-23 18:22:03 +01:00
1e8555a38e Merge pull request #4818 from dvdksn/docs-dockerd-host-gateway-ip-daemonconfig
docs: update host-gateway-ip to use daemon.json instead of cli flag
2024-01-23 15:55:03 +01:00
ec0a62436e docs: update host-gateway-ip to use daemon.json instead of cli flag
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-01-23 15:45:59 +01:00
469bfc05ed tests: add tests for cli-plugins/socket
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-01-23 14:19:33 +00:00
c89975d531 Merge pull request #4814 from thaJeztah/update_compose
Dockerfile: update docker compose to v2.24.2
2024-01-23 11:04:31 +01:00
091af560ca Dockerfile: update docker compose to v2.24.2
Update the version of compose used in CI to the latest version.

- full diff: docker/compose@v2.24.0...v2.24.2
- release notes: https://github.com/docker/compose/releases/tag/v2.24.1
- release notes: https://github.com/docker/compose/releases/tag/v2.24.2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-23 10:32:22 +01:00
2402dac819 cli/command/container: remove deprecated NewStartOptions
This function was deprecated in 298bddcc23 for
v25.0, and unused. This patch removes the function.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-20 21:48:46 +01:00
b43377a38b cli/command: remove deprecated DockerCliOption, InitializeOpt
These types were deprecated in 7af509c7f1 (v25.0),
in favor of CLIOption, and are no longer used.

This patch removes the deprecated type-aliases, and while updating, also improves
the documentation for the CLIOption type.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-20 21:39:32 +01:00
a71d39bcad info: remove printServerWarningsLegacy
Docker Engine 18.09 (API v1.39) introduced a Warnings field in the into response.
This enhancement was not gated by API version (see [moby/moby@a3d4238]), and
will be returned by Docker Engine 18.09 and up, regardless of the API version
chosen.

Likewise, the client-side code was written to prefer warnings returned by
the daemon, but to fall back on client-side detection of missing features
based on information in the Info response (see [docker/cli@3c27ce2]).

Thse warnings are purely informational, and given that Docker Engine versions
before 18.09 have reached EOL 6 Years ago, and any current version of the
Engine now returns the Warnings, it should be safe to remove the client-side
fall back logic.

This patch removes the client-side fall back code for warnings that was
added in 3c27ce21c9.

[moby/moby@a3d4238]: a3d4238b9c
[docker/cli@3c27ce2]: 3c27ce21c9

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-20 20:45:59 +01:00
f18a476b6d Merge pull request #4805 from tonistiigi/socket-eof-return
socket: return from loop after EOF
2024-01-20 13:44:45 +01:00
8cd3b00420 socket: return from loop after EOF
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2024-01-19 17:06:43 -08:00
e2519aefcb Merge pull request #4802 from thaJeztah/update_engine
vendor: github.com/docker/docker v25.0.0
2024-01-19 15:31:23 +01:00
1c73abb634 Merge pull request #4800 from dvdksn/docs-fix-broken-alias-links
docs: fix broken links to alias pages
2024-01-19 15:06:33 +01:00
337dd82d8b vendor: github.com/docker/docker v25.0.0
full diff: https://github.com/docker/docker/compare/v25.0.0-rc.3...v25.0.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-19 15:03:52 +01:00
d633890f91 docs: move base command to docker.md
CLI reference for the base command was generated to cli.md

Changed it to docker.md to handle broken links.

Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-01-19 14:44:35 +01:00
ca95badb36 Merge pull request #4799 from dvdksn/docs-cdi
docs: add documentation for CDI
2024-01-19 14:15:00 +01:00
aff4649cb7 docs: fix broken links to alias pages
Alias pages redirect to the canonical names, but these pages still
linked to the aliases, causing broken links when building the docs site.

Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-01-19 14:06:29 +01:00
852d198bb5 docs: add documentation for CDI
- Add section about cdi-spec-dirs daemon configuration
- Add subsection about cdi in --device section for docker run
- Update `docker info` output example

Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-01-18 14:58:43 +01:00
1f9573bb05 Add Linode docker volume plugin
Signed-off-by: Zhiwei Liang <zliang@akamai.com>
2023-06-30 21:39:58 -04:00
341 changed files with 6201 additions and 2546 deletions

View File

@ -1,19 +0,0 @@
# This is a dummy CircleCI config file to avoid GitHub status failures reported
# on branches that don't use CircleCI. This file should be deleted when all
# branches are no longer dependent on CircleCI.
version: 2
jobs:
dummy:
docker:
- image: busybox
steps:
- run:
name: "dummy"
command: echo "dummy job"
workflows:
version: 2
ci:
jobs:
- dummy

View File

@ -19,7 +19,7 @@ on:
jobs:
prepare:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
outputs:
matrix: ${{ steps.platforms.outputs.matrix }}
steps:
@ -37,7 +37,7 @@ jobs:
echo ${{ steps.platforms.outputs.matrix }}
build:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs:
- prepare
strategy:
@ -90,7 +90,7 @@ jobs:
if-no-files-found: error
bin-image:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
steps:
-
@ -134,7 +134,7 @@ jobs:
*.cache-to=type=gha,scope=bin-image,mode=max
prepare-plugins:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
outputs:
matrix: ${{ steps.platforms.outputs.matrix }}
steps:
@ -152,7 +152,7 @@ jobs:
echo ${{ steps.platforms.outputs.matrix }}
plugins:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs:
- prepare-plugins
strategy:

View File

@ -16,7 +16,7 @@ on:
jobs:
e2e:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
@ -28,11 +28,11 @@ jobs:
- alpine
- debian
engine-version:
# - 20.10-dind # FIXME: Fails on 20.10
- stable-dind # TODO: Use 20.10-dind, stable-dind is deprecated
include:
- target: non-experimental
engine-version: 19.03-dind
- 25.0 # latest
- 24.0 # latest - 1
- 23.0 # mirantis lts
# TODO(krissetto) 19.03 needs a look, doesn't work ubuntu 22.04 (cgroup errors).
# we could have a separate job that tests it against ubuntu 20.04
steps:
-
name: Checkout
@ -55,10 +55,11 @@ jobs:
make -f docker.Makefile test-e2e-${{ matrix.target }}
env:
BASE_VARIANT: ${{ matrix.base }}
E2E_ENGINE_VERSION: ${{ matrix.engine-version }}
ENGINE_VERSION: ${{ matrix.engine-version }}
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
-
name: Send to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
file: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -16,7 +16,7 @@ on:
jobs:
ctn:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
-
name: Checkout
@ -31,9 +31,10 @@ jobs:
targets: test-coverage
-
name: Send to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
file: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
host:
runs-on: ${{ matrix.os }}
@ -63,7 +64,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.21.6
go-version: 1.21.8
-
name: Test
run: |
@ -73,7 +74,8 @@ jobs:
shell: bash
-
name: Send to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
file: /tmp/coverage.txt
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -16,7 +16,7 @@ on:
jobs:
validate:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
@ -37,7 +37,7 @@ jobs:
# check that the generated Markdown and the checked-in files match
validate-md:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
-
name: Checkout
@ -57,7 +57,7 @@ jobs:
fi
validate-make:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:

View File

@ -4,12 +4,12 @@ ARG BASE_VARIANT=alpine
ARG ALPINE_VERSION=3.18
ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.21.6
ARG GO_VERSION=1.21.8
ARG XX_VERSION=1.2.1
ARG GOVERSIONINFO_VERSION=v1.3.0
ARG GOTESTSUM_VERSION=v1.10.0
ARG BUILDX_VERSION=0.12.1
ARG COMPOSE_VERSION=v2.24.0
ARG COMPOSE_VERSION=v2.24.3
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx

View File

@ -2,11 +2,14 @@ package manager
import (
"fmt"
"net/url"
"os"
"strings"
"sync"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel/attribute"
)
const (
@ -30,6 +33,10 @@ const (
// 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
@ -98,3 +105,44 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e
})
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

@ -17,11 +17,17 @@ import (
"golang.org/x/sync/errgroup"
)
// 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.
const ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
const (
// 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"
// ResourceAttributesEnvvar is the name of the envvar that includes additional
// resource attributes for OTEL.
ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES"
)
// errPluginNotFound is the error returned when a plugin could not be found.
type errPluginNotFound string
@ -236,6 +242,7 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
return cmd, nil
}

View File

@ -1,12 +1,12 @@
package socket
import (
"crypto/rand"
"encoding/hex"
"errors"
"io"
"net"
"os"
"github.com/docker/distribution/uuid"
)
// EnvKey represents the well-known environment variable used to pass the plugin being
@ -17,7 +17,7 @@ const EnvKey = "DOCKER_CLI_PLUGIN_SOCKET"
// and update the conn pointer, and returns the listener for the socket (which the caller
// is responsible for closing when it's no longer needed).
func SetupConn(conn **net.UnixConn) (*net.UnixListener, error) {
listener, err := listen("docker_cli_" + uuid.Generate().String())
listener, err := listen("docker_cli_" + randomID())
if err != nil {
return nil, err
}
@ -27,6 +27,14 @@ func SetupConn(conn **net.UnixConn) (*net.UnixListener, error) {
return listener, nil
}
func randomID() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(err) // This shouldn't happen
}
return hex.EncodeToString(b)
}
func accept(listener *net.UnixListener, conn **net.UnixConn) {
go func() {
for {
@ -65,6 +73,7 @@ func ConnectAndWait(cb func()) {
_, err := conn.Read(b)
if errors.Is(err, io.EOF) {
cb()
return
}
}
}()

View File

@ -1,4 +1,4 @@
//go:build !darwin
//go:build !darwin && !openbsd
package socket
@ -15,5 +15,6 @@ func listen(socketname string) (*net.UnixListener, error) {
func onAccept(conn *net.UnixConn, listener *net.UnixListener) {
// do nothing
// while on darwin we would unlink here; on non-darwin the socket is abstract and not present on the filesystem
// while on darwin and OpenBSD we would unlink here;
// on non-darwin the socket is abstract and not present on the filesystem
}

View File

@ -0,0 +1,19 @@
package socket
import (
"net"
"os"
"path/filepath"
"syscall"
)
func listen(socketname string) (*net.UnixListener, error) {
return net.ListenUnix("unix", &net.UnixAddr{
Name: filepath.Join(os.TempDir(), socketname),
Net: "unix",
})
}
func onAccept(conn *net.UnixConn, listener *net.UnixListener) {
syscall.Unlink(listener.Addr().String())
}

View File

@ -0,0 +1,133 @@
package socket
import (
"io/fs"
"net"
"os"
"runtime"
"strings"
"testing"
"time"
"gotest.tools/v3/assert"
"gotest.tools/v3/poll"
)
func TestSetupConn(t *testing.T) {
t.Run("updates conn when connected", func(t *testing.T) {
var conn *net.UnixConn
listener, err := SetupConn(&conn)
assert.NilError(t, err)
assert.Check(t, listener != nil, "returned nil listener but no error")
addr, err := net.ResolveUnixAddr("unix", listener.Addr().String())
assert.NilError(t, err, "failed to resolve listener address")
_, err = net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to dial returned listener")
pollConnNotNil(t, &conn)
})
t.Run("allows reconnects", func(t *testing.T) {
var conn *net.UnixConn
listener, err := SetupConn(&conn)
assert.NilError(t, err)
assert.Check(t, listener != nil, "returned nil listener but no error")
addr, err := net.ResolveUnixAddr("unix", listener.Addr().String())
assert.NilError(t, err, "failed to resolve listener address")
otherConn, err := net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to dial returned listener")
otherConn.Close()
_, err = net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to redial listener")
})
t.Run("does not leak sockets to local directory", func(t *testing.T) {
var conn *net.UnixConn
listener, err := SetupConn(&conn)
assert.NilError(t, err)
assert.Check(t, listener != nil, "returned nil listener but no error")
checkDirNoPluginSocket(t)
addr, err := net.ResolveUnixAddr("unix", listener.Addr().String())
assert.NilError(t, err, "failed to resolve listener address")
_, err = net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to dial returned listener")
checkDirNoPluginSocket(t)
})
}
func checkDirNoPluginSocket(t *testing.T) {
t.Helper()
files, err := os.ReadDir(".")
assert.NilError(t, err, "failed to list files in dir to check for leaked sockets")
for _, f := range files {
info, err := f.Info()
assert.NilError(t, err, "failed to check file info")
// check for a socket with `docker_cli_` in the name (from `SetupConn()`)
if strings.Contains(f.Name(), "docker_cli_") && info.Mode().Type() == fs.ModeSocket {
t.Fatal("found socket in a local directory")
}
}
}
func TestConnectAndWait(t *testing.T) {
t.Run("calls cancel func on EOF", func(t *testing.T) {
var conn *net.UnixConn
listener, err := SetupConn(&conn)
assert.NilError(t, err, "failed to setup listener")
done := make(chan struct{})
t.Setenv(EnvKey, listener.Addr().String())
cancelFunc := func() {
done <- struct{}{}
}
ConnectAndWait(cancelFunc)
pollConnNotNil(t, &conn)
conn.Close()
select {
case <-done:
case <-time.After(10 * time.Millisecond):
t.Fatal("cancel function not closed after 10ms")
}
})
// TODO: this test cannot be executed with `t.Parallel()`, due to
// relying on goroutine numbers to ensure correct behaviour
t.Run("connect goroutine exits after EOF", func(t *testing.T) {
var conn *net.UnixConn
listener, err := SetupConn(&conn)
assert.NilError(t, err, "failed to setup listener")
t.Setenv(EnvKey, listener.Addr().String())
numGoroutines := runtime.NumGoroutine()
ConnectAndWait(func() {})
assert.Equal(t, runtime.NumGoroutine(), numGoroutines+1)
pollConnNotNil(t, &conn)
conn.Close()
poll.WaitOn(t, func(t poll.LogT) poll.Result {
if runtime.NumGoroutine() > numGoroutines+1 {
return poll.Continue("waiting for connect goroutine to exit")
}
return poll.Success()
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(10*time.Millisecond))
})
}
func pollConnNotNil(t *testing.T, conn **net.UnixConn) {
t.Helper()
poll.WaitOn(t, func(t poll.LogT) poll.Result {
if *conn == nil {
return poll.Continue("waiting for conn to not be nil")
}
return poll.Success()
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(10*time.Millisecond))
}

View File

@ -470,7 +470,7 @@ Common Commands:
Management Commands:
{{- range managementSubCommands . }}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
{{- end}}
{{- end}}
@ -479,7 +479,7 @@ Management Commands:
Swarm Commands:
{{- range orchestratorSubCommands . }}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
{{- end}}
{{- end}}

View File

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

View File

@ -66,8 +66,10 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
if options.all {
warning = allCacheWarning
}
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
return 0, "", nil
if !options.force {
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
return 0, "", err
}
}
report, err := dockerCli.Client().BuildCachePrune(ctx, types.BuildCachePruneOptions{

View File

@ -0,0 +1,28 @@
package builder
import (
"context"
"errors"
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"gotest.tools/v3/assert"
)
func TestBuilderPromptTermination(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
builderPruneFunc: func(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
return nil, errors.New("fakeClient builderPruneFunc should not be called")
},
})
cmd := NewPruneCommand(cli)
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
t.Helper()
assert.ErrorIs(t, err, command.ErrPromptTerminated)
})
}

View File

@ -35,7 +35,7 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
flags := cmd.Flags()
flags.BoolVar(&opts.leaveRunning, "leave-running", false, "Leave the container running after checkpoint")
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
return cmd
}

View File

@ -30,7 +30,7 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
}
flags := cmd.Flags()
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
return cmd
}

View File

@ -27,7 +27,7 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
}
flags := cmd.Flags()
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
return cmd
}

View File

@ -11,19 +11,11 @@ import (
"github.com/moby/term"
)
// CLIOption applies a modification on a DockerCli.
// CLIOption is a functional argument to apply options to a [DockerCli]. These
// options can be passed to [NewDockerCli] to initialize a new CLI, or
// applied with [DockerCli.Initialize] or [DockerCli.Apply].
type CLIOption func(cli *DockerCli) error
// DockerCliOption applies a modification on a DockerCli.
//
// Deprecated: use [CLIOption] instead.
type DockerCliOption = CLIOption
// InitializeOpt is the type of the functional options passed to DockerCli.Initialize
//
// Deprecated: use [CLIOption] instead.
type InitializeOpt = CLIOption
// WithStandardStreams sets a cli in, out and err streams with the standard streams.
func WithStandardStreams() CLIOption {
return func(cli *DockerCli) error {

View File

@ -7,6 +7,7 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/volume"
"github.com/spf13/cobra"
)
@ -17,7 +18,7 @@ type ValidArgsFn func(cmd *cobra.Command, args []string, toComplete string) ([]s
// ImageNames offers completion for images present within the local store
func ImageNames(dockerCli command.Cli) ValidArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, err := dockerCli.Client().ImageList(cmd.Context(), types.ImageListOptions{})
list, err := dockerCli.Client().ImageList(cmd.Context(), image.ListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}

View File

@ -38,7 +38,7 @@ func newConfigListCommand(dockerCli command.Cli) *cobra.Command {
flags := cmd.Flags()
flags.BoolVarP(&listOpts.Quiet, "quiet", "q", false, "Only display IDs")
flags.StringVarP(&listOpts.Format, "format", "", "", flagsHelper.FormatHelp)
flags.StringVar(&listOpts.Format, "format", "", flagsHelper.FormatHelp)
flags.VarP(&listOpts.Filter, "filter", "f", "Filter output based on conditions provided")
return cmd

View File

@ -43,22 +43,21 @@ func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClie
}
// NewAttachCommand creates a new cobra.Command for `docker attach`
func NewAttachCommand(dockerCli command.Cli) *cobra.Command {
func NewAttachCommand(dockerCLI command.Cli) *cobra.Command {
var opts AttachOptions
var ctr string
cmd := &cobra.Command{
Use: "attach [OPTIONS] CONTAINER",
Short: "Attach local standard input, output, and error streams to a running container",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctr = args[0]
return RunAttach(cmd.Context(), dockerCli, ctr, &opts)
containerID := args[0]
return RunAttach(cmd.Context(), dockerCLI, containerID, &opts)
},
Annotations: map[string]string{
"aliases": "docker container attach, docker attach",
},
ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(ctr types.Container) bool {
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr types.Container) bool {
return ctr.State != "paused"
}),
}
@ -71,13 +70,13 @@ func NewAttachCommand(dockerCli command.Cli) *cobra.Command {
}
// RunAttach executes an `attach` command
func RunAttach(ctx context.Context, dockerCLI command.Cli, target string, opts *AttachOptions) error {
func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, opts *AttachOptions) error {
apiClient := dockerCLI.Client()
// request channel to wait for client
resultC, errC := apiClient.ContainerWait(ctx, target, "")
resultC, errC := apiClient.ContainerWait(ctx, containerID, "")
c, err := inspectContainerAndCheckState(ctx, apiClient, target)
c, err := inspectContainerAndCheckState(ctx, apiClient, containerID)
if err != nil {
return err
}
@ -106,11 +105,11 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, target string, opts *
if opts.Proxy && !c.Config.Tty {
sigc := notifyAllSignals()
go ForwardAllSignals(ctx, apiClient, target, sigc)
go ForwardAllSignals(ctx, apiClient, containerID, sigc)
defer signal.StopCatch(sigc)
}
resp, errAttach := apiClient.ContainerAttach(ctx, target, options)
resp, errAttach := apiClient.ContainerAttach(ctx, containerID, options)
if errAttach != nil {
return errAttach
}
@ -124,13 +123,13 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, target string, opts *
// the container and not exit.
//
// Recheck the container's state to avoid attach block.
_, err = inspectContainerAndCheckState(ctx, apiClient, target)
_, err = inspectContainerAndCheckState(ctx, apiClient, containerID)
if err != nil {
return err
}
if c.Config.Tty && dockerCLI.Out().IsTerminal() {
resizeTTY(ctx, dockerCLI, target)
resizeTTY(ctx, dockerCLI, containerID)
}
streamer := hijackedIOStreamer{

View File

@ -6,6 +6,8 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/client"
@ -23,7 +25,7 @@ type fakeClient struct {
platform *specs.Platform,
containerName string) (container.CreateResponse, error)
containerStartFunc func(containerID string, options container.StartOptions) error
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
imageCreateFunc func(parentReference string, options image.CreateOptions) (io.ReadCloser, error)
infoFunc func() (system.Info, error)
containerStatPathFunc func(containerID, path string) (types.ContainerPathStat, error)
containerCopyFromFunc func(containerID, srcPath string) (io.ReadCloser, types.ContainerPathStat, error)
@ -34,6 +36,7 @@ type fakeClient struct {
containerExecResizeFunc func(id string, options container.ResizeOptions) error
containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error
containerKillFunc func(ctx context.Context, containerID, signal string) error
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error)
Version string
}
@ -90,7 +93,7 @@ func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, op
return nil
}
func (f *fakeClient) ImageCreate(_ context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
func (f *fakeClient) ImageCreate(_ context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
if f.imageCreateFunc != nil {
return f.imageCreateFunc(parentReference, options)
}
@ -163,3 +166,10 @@ func (f *fakeClient) ContainerKill(ctx context.Context, containerID, signal stri
}
return nil
}
func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) {
if f.containerPruneFunc != nil {
return f.containerPruneFunc(ctx, pruneFilters)
}
return types.ContainersPruneReport{}, nil
}

View File

@ -15,8 +15,8 @@ import (
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
imagetypes "github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/jsonmessage"
@ -119,7 +119,7 @@ func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *
return err
}
responseBody, err := dockerCli.Client().ImageCreate(ctx, img, types.ImageCreateOptions{
responseBody, err := dockerCli.Client().ImageCreate(ctx, img, imagetypes.CreateOptions{
RegistryAuth: encodedAuth,
Platform: options.platform,
})

View File

@ -15,8 +15,8 @@ import (
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/system"
"github.com/google/go-cmp/cmp"
@ -134,7 +134,7 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
return container.CreateResponse{ID: containerID}, nil
}
},
imageCreateFunc: func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
imageCreateFunc: func(parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
defer func() { pullCounter++ }()
return io.NopCloser(strings.NewReader("")), nil
},

View File

@ -66,12 +66,12 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
flags := cmd.Flags()
flags.SetInterspersed(false)
flags.StringVarP(&options.DetachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container")
flags.StringVar(&options.DetachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
flags.BoolVarP(&options.Interactive, "interactive", "i", false, "Keep STDIN open even if not attached")
flags.BoolVarP(&options.TTY, "tty", "t", false, "Allocate a pseudo-TTY")
flags.BoolVarP(&options.Detach, "detach", "d", false, "Detached mode: run command in the background")
flags.StringVarP(&options.User, "user", "u", "", `Username or UID (format: "<name|uid>[:<group|gid>]")`)
flags.BoolVarP(&options.Privileged, "privileged", "", false, "Give extended privileges to the command")
flags.BoolVar(&options.Privileged, "privileged", false, "Give extended privileges to the command")
flags.VarP(&options.Env, "env", "e", "Set environment variables")
flags.SetAnnotation("env", "version", []string{"1.25"})
flags.Var(&options.EnvFile, "env-file", "Read in a file of environment variables")

View File

@ -55,7 +55,7 @@ func NewPsCommand(dockerCLI command.Cli) *cobra.Command {
flags.BoolVar(&options.noTrunc, "no-trunc", false, "Don't truncate output")
flags.BoolVarP(&options.nLatest, "latest", "l", false, "Show the latest created container (includes all states)")
flags.IntVarP(&options.last, "last", "n", -1, "Show n last created containers (includes all states)")
flags.StringVarP(&options.format, "format", "", "", flagsHelper.FormatHelp)
flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
return cmd

View File

@ -53,8 +53,10 @@ Are you sure you want to continue?`
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
return 0, "", nil
if !options.force {
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
return 0, "", err
}
}
report, err := dockerCli.Client().ContainersPrune(ctx, pruneFilters)

View File

@ -0,0 +1,29 @@
package container
import (
"context"
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
)
func TestContainerPrunePromptTermination(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
containerPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) {
return types.ContainersPruneReport{}, errors.New("fakeClient containerPruneFunc should not be called")
},
})
cmd := NewPruneCommand(cli)
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
t.Helper()
assert.ErrorIs(t, err, command.ErrPromptTerminated)
})
}

View File

@ -28,13 +28,6 @@ type StartOptions struct {
Containers []string
}
// NewStartOptions creates a new StartOptions.
//
// Deprecated: create a new [StartOptions] directly.
func NewStartOptions() StartOptions {
return StartOptions{}
}
// NewStartCommand creates a new cobra.Command for `docker start`
func NewStartCommand(dockerCli command.Cli) *cobra.Command {
var opts StartOptions

View File

@ -17,15 +17,15 @@ type fakeClient struct {
client.Client
imageTagFunc func(string, string) error
imageSaveFunc func(images []string) (io.ReadCloser, error)
imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error)
imagePushFunc func(ref string, options types.ImagePushOptions) (io.ReadCloser, error)
imageRemoveFunc func(image string, options image.RemoveOptions) ([]image.DeleteResponse, error)
imagePushFunc func(ref string, options image.PushOptions) (io.ReadCloser, error)
infoFunc func() (system.Info, error)
imagePullFunc func(ref string, options types.ImagePullOptions) (io.ReadCloser, error)
imagePullFunc func(ref string, options image.PullOptions) (io.ReadCloser, error)
imagesPruneFunc func(pruneFilter filters.Args) (types.ImagesPruneReport, error)
imageLoadFunc func(input io.Reader, quiet bool) (types.ImageLoadResponse, error)
imageListFunc func(options types.ImageListOptions) ([]image.Summary, error)
imageListFunc func(options image.ListOptions) ([]image.Summary, error)
imageInspectFunc func(image string) (types.ImageInspect, []byte, error)
imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
imageImportFunc func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error)
imageHistoryFunc func(image string) ([]image.HistoryResponseItem, error)
imageBuildFunc func(context.Context, io.Reader, types.ImageBuildOptions) (types.ImageBuildResponse, error)
}
@ -45,7 +45,7 @@ func (cli *fakeClient) ImageSave(_ context.Context, images []string) (io.ReadClo
}
func (cli *fakeClient) ImageRemove(_ context.Context, img string,
options types.ImageRemoveOptions,
options image.RemoveOptions,
) ([]image.DeleteResponse, error) {
if cli.imageRemoveFunc != nil {
return cli.imageRemoveFunc(img, options)
@ -53,7 +53,7 @@ func (cli *fakeClient) ImageRemove(_ context.Context, img string,
return []image.DeleteResponse{}, nil
}
func (cli *fakeClient) ImagePush(_ context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
func (cli *fakeClient) ImagePush(_ context.Context, ref string, options image.PushOptions) (io.ReadCloser, error) {
if cli.imagePushFunc != nil {
return cli.imagePushFunc(ref, options)
}
@ -67,7 +67,7 @@ func (cli *fakeClient) Info(_ context.Context) (system.Info, error) {
return system.Info{}, nil
}
func (cli *fakeClient) ImagePull(_ context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
func (cli *fakeClient) ImagePull(_ context.Context, ref string, options image.PullOptions) (io.ReadCloser, error) {
if cli.imagePullFunc != nil {
cli.imagePullFunc(ref, options)
}
@ -88,7 +88,7 @@ func (cli *fakeClient) ImageLoad(_ context.Context, input io.Reader, quiet bool)
return types.ImageLoadResponse{}, nil
}
func (cli *fakeClient) ImageList(_ context.Context, options types.ImageListOptions) ([]image.Summary, error) {
func (cli *fakeClient) ImageList(_ context.Context, options image.ListOptions) ([]image.Summary, error) {
if cli.imageListFunc != nil {
return cli.imageListFunc(options)
}
@ -103,7 +103,7 @@ func (cli *fakeClient) ImageInspectWithRaw(_ context.Context, img string) (types
}
func (cli *fakeClient) ImageImport(_ context.Context, source types.ImageImportSource, ref string,
options types.ImageImportOptions,
options image.ImportOptions,
) (io.ReadCloser, error) {
if cli.imageImportFunc != nil {
return cli.imageImportFunc(source, ref, options)

View File

@ -9,6 +9,7 @@ import (
"github.com/docker/cli/cli/command"
dockeropts "github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/spf13/cobra"
)
@ -78,7 +79,7 @@ func runImport(ctx context.Context, dockerCli command.Cli, options importOptions
}
}
responseBody, err := dockerCli.Client().ImageImport(ctx, source, options.reference, types.ImageImportOptions{
responseBody, err := dockerCli.Client().ImageImport(ctx, source, options.reference, image.ImportOptions{
Message: options.message,
Changes: options.changes.GetAll(),
Platform: options.platform,

View File

@ -7,6 +7,7 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
@ -17,7 +18,7 @@ func TestNewImportCommandErrors(t *testing.T) {
name string
args []string
expectedError string
imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
imageImportFunc func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error)
}{
{
name: "wrong-args",
@ -28,7 +29,7 @@ func TestNewImportCommandErrors(t *testing.T) {
name: "import-failed",
args: []string{"testdata/import-command-success.input.txt"},
expectedError: "something went wrong",
imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
imageImportFunc: func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) {
return nil, errors.Errorf("something went wrong")
},
},
@ -52,7 +53,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
testCases := []struct {
name string
args []string
imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
imageImportFunc func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error)
}{
{
name: "simple",
@ -65,7 +66,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
{
name: "double",
args: []string{"-", "image:local"},
imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
imageImportFunc: func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) {
assert.Check(t, is.Equal("image:local", ref))
return io.NopCloser(strings.NewReader("")), nil
},
@ -73,7 +74,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
{
name: "message",
args: []string{"--message", "test message", "-"},
imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
imageImportFunc: func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) {
assert.Check(t, is.Equal("test message", options.Message))
return io.NopCloser(strings.NewReader("")), nil
},
@ -81,7 +82,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
{
name: "change",
args: []string{"--change", "ENV DEBUG=true", "-"},
imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
imageImportFunc: func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) {
assert.Check(t, is.Equal("ENV DEBUG=true", options.Changes[0]))
return io.NopCloser(strings.NewReader("")), nil
},
@ -89,7 +90,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
{
name: "change legacy syntax",
args: []string{"--change", "ENV DEBUG true", "-"},
imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
imageImportFunc: func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) {
assert.Check(t, is.Equal("ENV DEBUG true", options.Changes[0]))
return io.NopCloser(strings.NewReader("")), nil
},

View File

@ -2,13 +2,15 @@ package image
import (
"context"
"fmt"
"io"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"github.com/spf13/cobra"
)
@ -21,10 +23,11 @@ type imagesOptions struct {
showDigests bool
format string
filter opts.FilterOpt
calledAs string
}
// NewImagesCommand creates a new `docker images` command
func NewImagesCommand(dockerCli command.Cli) *cobra.Command {
func NewImagesCommand(dockerCLI command.Cli) *cobra.Command {
options := imagesOptions{filter: opts.NewFilterOpt()}
cmd := &cobra.Command{
@ -35,7 +38,11 @@ func NewImagesCommand(dockerCli command.Cli) *cobra.Command {
if len(args) > 0 {
options.matchName = args[0]
}
return runImages(cmd.Context(), dockerCli, options)
// Pass through how the command was invoked. We use this to print
// warnings when an ambiguous argument was passed when using the
// legacy (top-level) "docker images" subcommand.
options.calledAs = cmd.CalledAs()
return runImages(cmd.Context(), dockerCLI, options)
},
Annotations: map[string]string{
"category-top": "7",
@ -55,33 +62,31 @@ func NewImagesCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
func newListCommand(dockerCli command.Cli) *cobra.Command {
cmd := *NewImagesCommand(dockerCli)
func newListCommand(dockerCLI command.Cli) *cobra.Command {
cmd := *NewImagesCommand(dockerCLI)
cmd.Aliases = []string{"list"}
cmd.Use = "ls [OPTIONS] [REPOSITORY[:TAG]]"
return &cmd
}
func runImages(ctx context.Context, dockerCli command.Cli, options imagesOptions) error {
func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions) error {
filters := options.filter.Value()
if options.matchName != "" {
filters.Add("reference", options.matchName)
}
listOptions := types.ImageListOptions{
images, err := dockerCLI.Client().ImageList(ctx, image.ListOptions{
All: options.all,
Filters: filters,
}
images, err := dockerCli.Client().ImageList(ctx, listOptions)
})
if err != nil {
return err
}
format := options.format
if len(format) == 0 {
if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !options.quiet {
format = dockerCli.ConfigFile().ImagesFormat
if len(dockerCLI.ConfigFile().ImagesFormat) > 0 && !options.quiet {
format = dockerCLI.ConfigFile().ImagesFormat
} else {
format = formatter.TableFormatKey
}
@ -89,11 +94,50 @@ func runImages(ctx context.Context, dockerCli command.Cli, options imagesOptions
imageCtx := formatter.ImageContext{
Context: formatter.Context{
Output: dockerCli.Out(),
Output: dockerCLI.Out(),
Format: formatter.NewImageFormat(format, options.quiet, options.showDigests),
Trunc: !options.noTrunc,
},
Digest: options.showDigests,
}
return formatter.ImageWrite(imageCtx, images)
if err := formatter.ImageWrite(imageCtx, images); err != nil {
return err
}
if options.matchName != "" && len(images) == 0 && options.calledAs == "images" {
printAmbiguousHint(dockerCLI.Err(), options.matchName)
}
return nil
}
// printAmbiguousHint prints an informational warning if the provided filter
// argument is ambiguous.
//
// The "docker images" top-level subcommand predates the "docker <object> <verb>"
// convention (e.g. "docker image ls"), but accepts a positional argument to
// search/filter images by name (globbing). It's common for users to accidentally
// mistake these commands, and to use (e.g.) "docker images ls", expecting
// to see all images, but ending up with an empty list because no image named
// "ls" was found.
//
// Disallowing these search-terms would be a breaking change, but we can print
// and informational message to help the users correct their mistake.
func printAmbiguousHint(stdErr io.Writer, matchName string) {
switch matchName {
// List of subcommands for "docker image" and their aliases (see "docker image --help"):
case "build",
"history",
"import",
"inspect",
"list",
"load",
"ls",
"prune",
"pull",
"push",
"rm",
"save",
"tag":
_, _ = fmt.Fprintf(stdErr, "\nNo images found matching %q: did you mean \"docker image %[1]s\"?\n", matchName)
}
}

View File

@ -7,7 +7,6 @@ import (
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
@ -20,7 +19,7 @@ func TestNewImagesCommandErrors(t *testing.T) {
name string
args []string
expectedError string
imageListFunc func(options types.ImageListOptions) ([]image.Summary, error)
imageListFunc func(options image.ListOptions) ([]image.Summary, error)
}{
{
name: "wrong-args",
@ -30,7 +29,7 @@ func TestNewImagesCommandErrors(t *testing.T) {
{
name: "failed-list",
expectedError: "something went wrong",
imageListFunc: func(options types.ImageListOptions) ([]image.Summary, error) {
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
return []image.Summary{}, errors.Errorf("something went wrong")
},
},
@ -48,7 +47,7 @@ func TestNewImagesCommandSuccess(t *testing.T) {
name string
args []string
imageFormat string
imageListFunc func(options types.ImageListOptions) ([]image.Summary, error)
imageListFunc func(options image.ListOptions) ([]image.Summary, error)
}{
{
name: "simple",
@ -65,7 +64,7 @@ func TestNewImagesCommandSuccess(t *testing.T) {
{
name: "match-name",
args: []string{"image"},
imageListFunc: func(options types.ImageListOptions) ([]image.Summary, error) {
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
assert.Check(t, is.Equal("image", options.Filters.Get("reference")[0]))
return []image.Summary{}, nil
},
@ -73,7 +72,7 @@ func TestNewImagesCommandSuccess(t *testing.T) {
{
name: "filters",
args: []string{"--filter", "name=value"},
imageListFunc: func(options types.ImageListOptions) ([]image.Summary, error) {
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
assert.Check(t, is.Equal("value", options.Filters.Get("name")[0]))
return []image.Summary{}, nil
},
@ -96,3 +95,17 @@ func TestNewListCommandAlias(t *testing.T) {
assert.Check(t, cmd.HasAlias("list"))
assert.Check(t, !cmd.HasAlias("other"))
}
func TestNewListCommandAmbiguous(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cmd := NewImagesCommand(cli)
cmd.SetOut(io.Discard)
// Set the Use field to mimic that the command was called as "docker images",
// not "docker image ls".
cmd.Use = "images"
cmd.SetArgs([]string{"ls"})
err := cmd.Execute()
assert.NilError(t, err)
golden.Assert(t, cli.ErrBuffer().String(), "list-command-ambiguous.golden")
}

View File

@ -67,8 +67,10 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
if options.all {
warning = allImageWarning
}
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
return 0, "", nil
if !options.force {
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
return 0, "", err
}
}
report, err := dockerCli.Client().ImagesPrune(ctx, pruneFilters)

View File

@ -1,10 +1,12 @@
package image
import (
"context"
"fmt"
"io"
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
@ -101,3 +103,19 @@ func TestNewPruneCommandSuccess(t *testing.T) {
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("prune-command-success.%s.golden", tc.name))
}
}
func TestPrunePromptTermination(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) {
return types.ImagesPruneReport{}, errors.New("fakeClient imagesPruneFunc should not be called")
},
})
cmd := NewPruneCommand(cli)
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
t.Helper()
assert.ErrorIs(t, err, command.ErrPromptTerminated)
})
}

View File

@ -8,7 +8,7 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
@ -69,7 +69,7 @@ func TestNewPullCommandSuccess(t *testing.T) {
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
imagePullFunc: func(ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) {
assert.Check(t, is.Equal(tc.expectedTag, ref), tc.name)
return io.NopCloser(strings.NewReader("")), nil
},
@ -111,7 +111,7 @@ func TestNewPullCommandWithContentTrustErrors(t *testing.T) {
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
imagePullFunc: func(ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("")), fmt.Errorf("shouldn't try to pull image")
},
}, test.EnableContentTrust)

View File

@ -10,7 +10,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/streams"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/registry"
@ -80,7 +80,7 @@ func RunPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error
return err
}
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "push")
options := types.ImagePushOptions{
options := image.PushOptions{
All: opts.all,
RegistryAuth: encodedAuth,
PrivilegeFunc: requestPrivilege,

View File

@ -6,7 +6,7 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
)
@ -16,7 +16,7 @@ func TestNewPushCommandErrors(t *testing.T) {
name string
args []string
expectedError string
imagePushFunc func(ref string, options types.ImagePushOptions) (io.ReadCloser, error)
imagePushFunc func(ref string, options image.PushOptions) (io.ReadCloser, error)
}{
{
name: "wrong-args",
@ -32,7 +32,7 @@ func TestNewPushCommandErrors(t *testing.T) {
name: "push-failed",
args: []string{"image:repo"},
expectedError: "Failed to push",
imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
imagePushFunc: func(ref string, options image.PushOptions) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("")), errors.Errorf("Failed to push")
},
},
@ -67,7 +67,7 @@ func TestNewPushCommandSuccess(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
imagePushFunc: func(ref string, options image.PushOptions) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("")), nil
},
})

View File

@ -7,7 +7,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/errdefs"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -52,7 +52,7 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
func runRemove(ctx context.Context, dockerCli command.Cli, opts removeOptions, images []string) error {
client := dockerCli.Client()
options := types.ImageRemoveOptions{
options := image.RemoveOptions{
Force: opts.force,
PruneChildren: !opts.noPrune,
}

View File

@ -6,7 +6,6 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
@ -36,7 +35,7 @@ func TestNewRemoveCommandErrors(t *testing.T) {
name string
args []string
expectedError string
imageRemoveFunc func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error)
imageRemoveFunc func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error)
}{
{
name: "wrong args",
@ -46,7 +45,7 @@ func TestNewRemoveCommandErrors(t *testing.T) {
name: "ImageRemove fail with force option",
args: []string{"-f", "image1"},
expectedError: "error removing image",
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
assert.Check(t, is.Equal("image1", img))
return []image.DeleteResponse{}, errors.Errorf("error removing image")
},
@ -55,7 +54,7 @@ func TestNewRemoveCommandErrors(t *testing.T) {
name: "ImageRemove fail",
args: []string{"arg1"},
expectedError: "error removing image",
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
assert.Check(t, !options.Force)
assert.Check(t, options.PruneChildren)
return []image.DeleteResponse{}, errors.Errorf("error removing image")
@ -78,13 +77,13 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
testCases := []struct {
name string
args []string
imageRemoveFunc func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error)
imageRemoveFunc func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error)
expectedStderr string
}{
{
name: "Image Deleted",
args: []string{"image1"},
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
assert.Check(t, is.Equal("image1", img))
return []image.DeleteResponse{{Deleted: img}}, nil
},
@ -92,7 +91,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
{
name: "Image not found with force option",
args: []string{"-f", "image1"},
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
assert.Check(t, is.Equal("image1", img))
assert.Check(t, is.Equal(true, options.Force))
return []image.DeleteResponse{}, notFound{"image1"}
@ -103,7 +102,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
{
name: "Image Untagged",
args: []string{"image1"},
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
assert.Check(t, is.Equal("image1", img))
return []image.DeleteResponse{{Untagged: img}}, nil
},
@ -111,7 +110,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
{
name: "Image Deleted and Untagged",
args: []string{"image1", "image2"},
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
if img == "image1" {
return []image.DeleteResponse{{Untagged: img}}, nil
}

View File

@ -5,7 +5,6 @@
"RepoDigests": null,
"Parent": "",
"Comment": "",
"Created": "",
"Container": "",
"ContainerConfig": null,
"DockerVersion": "",
@ -29,7 +28,6 @@
"RepoDigests": null,
"Parent": "",
"Comment": "",
"Created": "",
"Container": "",
"ContainerConfig": null,
"DockerVersion": "",

View File

@ -5,7 +5,6 @@
"RepoDigests": null,
"Parent": "",
"Comment": "",
"Created": "",
"Container": "",
"ContainerConfig": null,
"DockerVersion": "",

View File

@ -0,0 +1,2 @@
No images found matching "ls": did you mean "docker image ls"?

View File

@ -13,6 +13,7 @@ import (
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/trust"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/registry"
@ -30,7 +31,7 @@ type target struct {
}
// TrustedPush handles content trust pushing of an image
func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, options types.ImagePushOptions) error {
func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, options image.PushOptions) error {
responseBody, err := cli.Client().ImagePush(ctx, reference.FamiliarString(ref), options)
if err != nil {
return err
@ -267,7 +268,7 @@ func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth tru
return err
}
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(cli, imgRefAndAuth.RepoInfo().Index, "pull")
responseBody, err := cli.Client().ImagePull(ctx, reference.FamiliarString(imgRefAndAuth.Reference()), types.ImagePullOptions{
responseBody, err := cli.Client().ImagePull(ctx, reference.FamiliarString(imgRefAndAuth.Reference()), image.PullOptions{
RegistryAuth: encodedAuth,
PrivilegeFunc: requestPrivilege,
All: opts.all,

View File

@ -4,6 +4,7 @@ import (
"context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
)
@ -15,6 +16,8 @@ type fakeClient struct {
networkDisconnectFunc func(ctx context.Context, networkID, container string, force bool) error
networkRemoveFunc func(ctx context.Context, networkID string) error
networkListFunc func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error)
networkPruneFunc func(ctx context.Context, pruneFilters filters.Args) (types.NetworksPruneReport, error)
networkInspectFunc func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, []byte, error)
}
func (c *fakeClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {
@ -52,6 +55,16 @@ func (c *fakeClient) NetworkRemove(ctx context.Context, networkID string) error
return nil
}
func (c *fakeClient) NetworkInspectWithRaw(context.Context, string, types.NetworkInspectOptions) (types.NetworkResource, []byte, error) {
func (c *fakeClient) NetworkInspectWithRaw(ctx context.Context, networkID string, opts types.NetworkInspectOptions) (types.NetworkResource, []byte, error) {
if c.networkInspectFunc != nil {
return c.networkInspectFunc(ctx, networkID, opts)
}
return types.NetworkResource{}, nil, nil
}
func (c *fakeClient) NetworksPrune(ctx context.Context, pruneFilter filters.Args) (types.NetworksPruneReport, error) {
if c.networkPruneFunc != nil {
return c.networkPruneFunc(ctx, pruneFilter)
}
return types.NetworksPruneReport{}, nil
}

View File

@ -49,8 +49,10 @@ Are you sure you want to continue?`
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (output string, err error) {
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
return "", nil
if !options.force {
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
return "", err
}
}
report, err := dockerCli.Client().NetworksPrune(ctx, pruneFilters)

View File

@ -0,0 +1,29 @@
package network
import (
"context"
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
)
func TestNetworkPrunePromptTermination(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
networkPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (types.NetworksPruneReport, error) {
return types.NetworksPruneReport{}, errors.New("fakeClient networkPruneFunc should not be called")
},
})
cmd := NewPruneCommand(cli)
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
t.Helper()
assert.ErrorIs(t, err, command.ErrPromptTerminated)
})
}

View File

@ -46,10 +46,15 @@ func runRemove(ctx context.Context, dockerCli command.Cli, networks []string, op
status := 0
for _, name := range networks {
if nw, _, err := client.NetworkInspectWithRaw(ctx, name, types.NetworkInspectOptions{}); err == nil &&
nw.Ingress &&
!command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), ingressWarning) {
continue
nw, _, err := client.NetworkInspectWithRaw(ctx, name, types.NetworkInspectOptions{})
if err == nil && nw.Ingress {
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), ingressWarning)
if err != nil {
return err
}
if !r {
continue
}
}
if err := client.NetworkRemove(ctx, name); err != nil {
if opts.force && errdefs.IsNotFound(err) {

View File

@ -5,7 +5,9 @@ import (
"io"
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/errdefs"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
@ -94,3 +96,27 @@ func TestNetworkRemoveForce(t *testing.T) {
})
}
}
func TestNetworkRemovePromptTermination(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
networkRemoveFunc: func(ctx context.Context, networkID string) error {
return errors.New("fakeClient networkRemoveFunc should not be called")
},
networkInspectFunc: func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, []byte, error) {
return types.NetworkResource{
ID: "existing-network",
Name: "existing-network",
Ingress: true,
}, nil, nil
},
})
cmd := newRemoveCommand(cli)
cmd.SetArgs([]string{"existing-network"})
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
t.Helper()
assert.ErrorIs(t, err, command.ErrPromptTerminated)
})
}

View File

@ -19,6 +19,7 @@ type fakeClient struct {
pluginInstallFunc func(name string, options types.PluginInstallOptions) (io.ReadCloser, error)
pluginListFunc func(filter filters.Args) (types.PluginsListResponse, error)
pluginInspectFunc func(name string) (*types.Plugin, []byte, error)
pluginUpgradeFunc func(name string, options types.PluginInstallOptions) (io.ReadCloser, error)
}
func (c *fakeClient) PluginCreate(_ context.Context, createContext io.Reader, createOptions types.PluginCreateOptions) error {
@ -75,3 +76,10 @@ func (c *fakeClient) PluginInspectWithRaw(_ context.Context, name string) (*type
func (c *fakeClient) Info(context.Context) (system.Info, error) {
return system.Info{}, nil
}
func (c *fakeClient) PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
if c.pluginUpgradeFunc != nil {
return c.pluginUpgradeFunc(name, options)
}
return nil, nil
}

View File

@ -142,6 +142,7 @@ func acceptPrivileges(dockerCli command.Cli, name string) func(privileges types.
for _, privilege := range privileges {
fmt.Fprintf(dockerCli.Out(), " - %s: %v\n", privilege.Name, privilege.Value)
}
return command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), "Do you grant the above permissions?"), nil
ctx := context.TODO()
return command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), "Do you grant the above permissions?")
}
}

View File

@ -0,0 +1,2 @@
Upgrading plugin foo/bar from localhost:5000/foo/bar:v0.1.0 to localhost:5000/foo/bar:v1.0.0
Plugin images do not match, are you sure? [y/N]

View File

@ -63,7 +63,10 @@ func runUpgrade(ctx context.Context, dockerCli command.Cli, opts pluginOptions)
fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, reference.FamiliarString(old), reference.FamiliarString(remote))
if !opts.skipRemoteCheck && remote.String() != old.String() {
if !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), "Plugin images do not match, are you sure?") {
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), "Plugin images do not match, are you sure?"); !r || err != nil {
if err != nil {
return errors.Wrap(err, "canceling upgrade request")
}
return errors.New("canceling upgrade request")
}
}

View File

@ -0,0 +1,42 @@
package plugin
import (
"context"
"io"
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
"gotest.tools/v3/golden"
)
func TestUpgradePromptTermination(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
pluginUpgradeFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
return nil, errors.New("should not be called")
},
pluginInspectFunc: func(name string) (*types.Plugin, []byte, error) {
return &types.Plugin{
ID: "5724e2c8652da337ab2eedd19fc6fc0ec908e4bd907c7421bf6a8dfc70c4c078",
Name: "foo/bar",
Enabled: false,
PluginReference: "localhost:5000/foo/bar:v0.1.0",
}, []byte{}, nil
},
})
cmd := newUpgradeCommand(cli)
// need to set a remote address that does not match the plugin
// reference sent by the `pluginInspectFunc`
cmd.SetArgs([]string{"foo/bar", "localhost:5000/foo/bar:v1.0.0"})
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
t.Helper()
assert.ErrorIs(t, err, command.ErrPromptTerminated)
})
golden.Assert(t, cli.OutBuffer().String(), "plugin-upgrade-terminate.golden")
}

View File

@ -55,7 +55,7 @@ func NewLoginCommand(dockerCli command.Cli) *cobra.Command {
flags.StringVarP(&opts.user, "username", "u", "", "Username")
flags.StringVarP(&opts.password, "password", "p", "", "Password")
flags.BoolVarP(&opts.passwordStdin, "password-stdin", "", false, "Take the password from stdin")
flags.BoolVar(&opts.passwordStdin, "password-stdin", false, "Take the password from stdin")
return cmd
}

View File

@ -38,7 +38,7 @@ func newSecretListCommand(dockerCli command.Cli) *cobra.Command {
flags := cmd.Flags()
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display IDs")
flags.StringVarP(&options.format, "format", "", "", flagsHelper.FormatHelp)
flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
return cmd

View File

@ -137,7 +137,7 @@ func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet,
return nil
}
return waitOnService(ctx, dockerCli, response.ID, opts.quiet)
return WaitOnService(ctx, dockerCli, response.ID, opts.quiet)
}
// setConfigs does double duty: it both sets the ConfigReferences of the

View File

@ -9,9 +9,9 @@ import (
"github.com/docker/docker/pkg/jsonmessage"
)
// waitOnService waits for the service to converge. It outputs a progress bar,
// WaitOnService waits for the service to converge. It outputs a progress bar,
// if appropriate based on the CLI flags.
func waitOnService(ctx context.Context, dockerCli command.Cli, serviceID string, quiet bool) error {
func WaitOnService(ctx context.Context, dockerCli command.Cli, serviceID string, quiet bool) error {
errChan := make(chan error, 1)
pipeReader, pipeWriter := io.Pipe()

View File

@ -143,7 +143,7 @@ func ServiceProgress(ctx context.Context, apiClient client.APIClient, serviceID
if converged && time.Since(convergedAt) >= monitor {
progressOut.WriteProgress(progress.Progress{
ID: "verify",
Action: "Service converged",
Action: fmt.Sprintf("Service %s converged", serviceID),
})
if message != nil {
progressOut.WriteProgress(*message)

View File

@ -62,5 +62,5 @@ func runRollback(ctx context.Context, dockerCli command.Cli, options *serviceOpt
return nil
}
return waitOnService(ctx, dockerCli, serviceID, options.quiet)
return WaitOnService(ctx, dockerCli, serviceID, options.quiet)
}

View File

@ -80,7 +80,7 @@ func runScale(ctx context.Context, dockerCli command.Cli, options *scaleOptions,
if len(serviceIDs) > 0 {
if !options.detach && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.29") {
for _, serviceID := range serviceIDs {
if err := waitOnService(ctx, dockerCli, serviceID, false); err != nil {
if err := WaitOnService(ctx, dockerCli, serviceID, false); err != nil {
errs = append(errs, fmt.Sprintf("%s: %v", serviceID, err))
}
}

View File

@ -249,7 +249,7 @@ func runUpdate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet,
return nil
}
return waitOnService(ctx, dockerCli, serviceID, options.quiet)
return WaitOnService(ctx, dockerCli, serviceID, options.quiet)
}
//nolint:gocyclo

View File

@ -26,7 +26,7 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command {
if err != nil {
return err
}
return swarm.RunDeploy(cmd.Context(), dockerCli, opts, config)
return swarm.RunDeploy(cmd.Context(), dockerCli, cmd.Flags(), &opts, config)
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completeNames(dockerCli)(cmd, args, toComplete)
@ -42,5 +42,7 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command {
flags.StringVar(&opts.ResolveImage, "resolve-image", swarm.ResolveImageAlways,
`Query the registry to resolve image digest and supported platforms ("`+swarm.ResolveImageAlways+`", "`+swarm.ResolveImageChanged+`", "`+swarm.ResolveImageNever+`")`)
flags.SetAnnotation("resolve-image", "version", []string{"1.30"})
flags.BoolVarP(&opts.Detach, "detach", "d", true, "Exit immediately instead of waiting for the stack services to converge")
flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Suppress progress output")
return cmd
}

View File

@ -9,6 +9,8 @@ type Deploy struct {
ResolveImage string
SendRegistryAuth bool
Prune bool
Detach bool
Quiet bool
}
// Config holds docker stack config options
@ -36,6 +38,7 @@ type PS struct {
// Remove holds docker stack remove options
type Remove struct {
Namespaces []string
Detach bool
}
// Services holds docker stack services options

View File

@ -27,5 +27,8 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
return completeNames(dockerCli)(cmd, args, toComplete)
},
}
flags := cmd.Flags()
flags.BoolVarP(&opts.Detach, "detach", "d", true, "Do not wait for stack removal")
return cmd
}

View File

@ -44,3 +44,7 @@ func getStackSecrets(ctx context.Context, apiclient client.APIClient, namespace
func getStackConfigs(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Config, error) {
return apiclient.ConfigList(ctx, types.ConfigListOptions{Filters: getStackFilter(namespace)})
}
func getStackTasks(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Task, error) {
return apiclient.TaskList(ctx, types.TaskListOptions{Filters: getStackFilter(namespace)})
}

View File

@ -11,6 +11,7 @@ import (
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
"github.com/pkg/errors"
"github.com/spf13/pflag"
)
// Resolve image constants
@ -22,8 +23,8 @@ const (
)
// RunDeploy is the swarm implementation of docker stack deploy
func RunDeploy(ctx context.Context, dockerCli command.Cli, opts options.Deploy, cfg *composetypes.Config) error {
if err := validateResolveImageFlag(&opts); err != nil {
func RunDeploy(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, opts *options.Deploy, cfg *composetypes.Config) error {
if err := validateResolveImageFlag(opts); err != nil {
return err
}
// client side image resolution should not be done when the supported
@ -32,6 +33,11 @@ func RunDeploy(ctx context.Context, dockerCli command.Cli, opts options.Deploy,
opts.ResolveImage = ResolveImageNever
}
if opts.Detach && !flags.Changed("detach") {
fmt.Fprintln(dockerCli.Err(), "Since --detach=false was not specified, tasks will be created in the background.\n"+
"In a future release, --detach=false will become the default.")
}
return deployCompose(ctx, dockerCli, opts, cfg)
}

View File

@ -2,9 +2,11 @@ package swarm
import (
"context"
"errors"
"fmt"
"github.com/docker/cli/cli/command"
servicecli "github.com/docker/cli/cli/command/service"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/compose/convert"
composetypes "github.com/docker/cli/cli/compose/types"
@ -13,10 +15,9 @@ import (
"github.com/docker/docker/api/types/swarm"
apiclient "github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/pkg/errors"
)
func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy, config *composetypes.Config) error {
func deployCompose(ctx context.Context, dockerCli command.Cli, opts *options.Deploy, config *composetypes.Config) error {
if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil {
return err
}
@ -60,7 +61,17 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Depl
if err != nil {
return err
}
return deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
serviceIDs, err := deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
if err != nil {
return err
}
if opts.Detach {
return nil
}
return waitOnServices(ctx, dockerCli, serviceIDs, opts.Quiet)
}
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
@ -87,11 +98,11 @@ func validateExternalNetworks(ctx context.Context, client apiclient.NetworkAPICl
network, err := client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{})
switch {
case errdefs.IsNotFound(err):
return errors.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed", networkName)
return fmt.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed", networkName)
case err != nil:
return err
case network.Scope != "swarm":
return errors.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of \"swarm\"", networkName, network.Scope)
return fmt.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of \"swarm\"", networkName, network.Scope)
}
}
return nil
@ -106,13 +117,13 @@ func createSecrets(ctx context.Context, dockerCli command.Cli, secrets []swarm.S
case err == nil:
// secret already exists, then we update that
if err := client.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil {
return errors.Wrapf(err, "failed to update secret %s", secretSpec.Name)
return fmt.Errorf("failed to update secret %s: %w", secretSpec.Name, err)
}
case errdefs.IsNotFound(err):
// secret does not exist, then we create a new one.
fmt.Fprintf(dockerCli.Out(), "Creating secret %s\n", secretSpec.Name)
if _, err := client.SecretCreate(ctx, secretSpec); err != nil {
return errors.Wrapf(err, "failed to create secret %s", secretSpec.Name)
return fmt.Errorf("failed to create secret %s: %w", secretSpec.Name, err)
}
default:
return err
@ -130,13 +141,13 @@ func createConfigs(ctx context.Context, dockerCli command.Cli, configs []swarm.C
case err == nil:
// config already exists, then we update that
if err := client.ConfigUpdate(ctx, config.ID, config.Meta.Version, configSpec); err != nil {
return errors.Wrapf(err, "failed to update config %s", configSpec.Name)
return fmt.Errorf("failed to update config %s: %w", configSpec.Name, err)
}
case errdefs.IsNotFound(err):
// config does not exist, then we create a new one.
fmt.Fprintf(dockerCli.Out(), "Creating config %s\n", configSpec.Name)
if _, err := client.ConfigCreate(ctx, configSpec); err != nil {
return errors.Wrapf(err, "failed to create config %s", configSpec.Name)
return fmt.Errorf("failed to create config %s: %w", configSpec.Name, err)
}
default:
return err
@ -169,19 +180,19 @@ func createNetworks(ctx context.Context, dockerCli command.Cli, namespace conver
fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name)
if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil {
return errors.Wrapf(err, "failed to create network %s", name)
return fmt.Errorf("failed to create network %s: %w", name, err)
}
}
return nil
}
func deployServices(ctx context.Context, dockerCli command.Cli, services map[string]swarm.ServiceSpec, namespace convert.Namespace, sendAuth bool, resolveImage string) error {
func deployServices(ctx context.Context, dockerCli command.Cli, services map[string]swarm.ServiceSpec, namespace convert.Namespace, sendAuth bool, resolveImage string) ([]string, error) {
apiClient := dockerCli.Client()
out := dockerCli.Out()
existingServices, err := getStackServices(ctx, apiClient, namespace.Name())
if err != nil {
return err
return nil, err
}
existingServiceMap := make(map[string]swarm.Service)
@ -189,6 +200,8 @@ func deployServices(ctx context.Context, dockerCli command.Cli, services map[str
existingServiceMap[service.Spec.Name] = service
}
var serviceIDs []string
for internalName, serviceSpec := range services {
var (
name = namespace.Scope(internalName)
@ -200,7 +213,7 @@ func deployServices(ctx context.Context, dockerCli command.Cli, services map[str
// Retrieve encoded auth token from the image reference
encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), image)
if err != nil {
return err
return nil, err
}
}
@ -241,12 +254,14 @@ func deployServices(ctx context.Context, dockerCli command.Cli, services map[str
response, err := apiClient.ServiceUpdate(ctx, service.ID, service.Version, serviceSpec, updateOpts)
if err != nil {
return errors.Wrapf(err, "failed to update service %s", name)
return nil, fmt.Errorf("failed to update service %s: %w", name, err)
}
for _, warning := range response.Warnings {
fmt.Fprintln(dockerCli.Err(), warning)
}
serviceIDs = append(serviceIDs, service.ID)
} else {
fmt.Fprintf(out, "Creating service %s\n", name)
@ -257,10 +272,29 @@ func deployServices(ctx context.Context, dockerCli command.Cli, services map[str
createOpts.QueryRegistry = true
}
if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil {
return errors.Wrapf(err, "failed to create service %s", name)
response, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts)
if err != nil {
return nil, fmt.Errorf("failed to create service %s: %w", name, err)
}
serviceIDs = append(serviceIDs, response.ID)
}
}
return serviceIDs, nil
}
func waitOnServices(ctx context.Context, dockerCli command.Cli, serviceIDs []string, quiet bool) error {
var errs []error
for _, serviceID := range serviceIDs {
if err := servicecli.WaitOnService(ctx, dockerCli, serviceID, quiet); err != nil {
errs = append(errs, fmt.Errorf("%s: %w", serviceID, err))
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}

View File

@ -99,7 +99,7 @@ func TestServiceUpdateResolveImageChanged(t *testing.T) {
},
},
}
err := deployServices(ctx, client, spec, namespace, false, ResolveImageChanged)
_, err := deployServices(ctx, client, spec, namespace, false, ResolveImageChanged)
assert.NilError(t, err)
assert.Check(t, is.Equal(receivedOptions.QueryRegistry, tc.expectedQueryRegistry))
assert.Check(t, is.Equal(receivedService.TaskTemplate.ContainerSpec.Image, tc.expectedImage))

View File

@ -11,6 +11,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
apiclient "github.com/docker/docker/client"
"github.com/pkg/errors"
)
@ -58,6 +59,14 @@ func RunRemove(ctx context.Context, dockerCli command.Cli, opts options.Remove)
if hasError {
errs = append(errs, fmt.Sprintf("Failed to remove some resources from stack: %s", namespace))
continue
}
if !opts.Detach {
err = waitOnTasks(ctx, client, namespace)
if err != nil {
errs = append(errs, fmt.Sprintf("Failed to wait on tasks of stack: %s: %s", namespace, err))
}
}
}
@ -137,3 +146,45 @@ func removeConfigs(
}
return hasError
}
var numberedStates = map[swarm.TaskState]int64{
swarm.TaskStateNew: 1,
swarm.TaskStateAllocated: 2,
swarm.TaskStatePending: 3,
swarm.TaskStateAssigned: 4,
swarm.TaskStateAccepted: 5,
swarm.TaskStatePreparing: 6,
swarm.TaskStateReady: 7,
swarm.TaskStateStarting: 8,
swarm.TaskStateRunning: 9,
swarm.TaskStateComplete: 10,
swarm.TaskStateShutdown: 11,
swarm.TaskStateFailed: 12,
swarm.TaskStateRejected: 13,
}
func terminalState(state swarm.TaskState) bool {
return numberedStates[state] > numberedStates[swarm.TaskStateRunning]
}
func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace string) error {
terminalStatesReached := 0
for {
tasks, err := getStackTasks(ctx, client, namespace)
if err != nil {
return fmt.Errorf("failed to get tasks: %w", err)
}
for _, task := range tasks {
if terminalState(task.Status.State) {
terminalStatesReached++
break
}
}
if terminalStatesReached == len(tasks) {
break
}
}
return nil
}

View File

@ -20,7 +20,7 @@ func equalCIDR(c1 net.IPNet, c2 net.IPNet) bool {
func setUpIPNetFlagSet(ipsp *[]net.IPNet) *pflag.FlagSet {
f := pflag.NewFlagSet("test", pflag.ContinueOnError)
f.VarP(newIPNetSliceValue([]net.IPNet{}, ipsp), "cidrs", "", "Command separated list!")
f.Var(newIPNetSliceValue([]net.IPNet{}, ipsp), "cidrs", "Command separated list!")
return f
}

View File

@ -5,15 +5,18 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
)
type fakeClient struct {
client.Client
version string
serverVersion func(ctx context.Context) (types.Version, error)
eventsFn func(context.Context, types.EventsOptions) (<-chan events.Message, <-chan error)
version string
serverVersion func(ctx context.Context) (types.Version, error)
eventsFn func(context.Context, types.EventsOptions) (<-chan events.Message, <-chan error)
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error)
networkPruneFunc func(ctx context.Context, pruneFilter filters.Args) (types.NetworksPruneReport, error)
}
func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) {
@ -27,3 +30,17 @@ func (cli *fakeClient) ClientVersion() string {
func (cli *fakeClient) Events(ctx context.Context, opts types.EventsOptions) (<-chan events.Message, <-chan error) {
return cli.eventsFn(ctx, opts)
}
func (cli *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) {
if cli.containerPruneFunc != nil {
return cli.containerPruneFunc(ctx, pruneFilters)
}
return types.ContainersPruneReport{}, nil
}
func (cli *fakeClient) NetworksPrune(ctx context.Context, pruneFilter filters.Args) (types.NetworksPruneReport, error) {
if cli.networkPruneFunc != nil {
return cli.networkPruneFunc(ctx, pruneFilter)
}
return types.NetworksPruneReport{}, nil
}

View File

@ -21,7 +21,6 @@ import (
"github.com/docker/cli/templates"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/registry"
"github.com/docker/go-units"
"github.com/spf13/cobra"
@ -380,7 +379,10 @@ func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) []error {
}
fprintln(output)
printServerWarnings(streams.Err(), info)
for _, w := range info.Warnings {
fprintln(streams.Err(), w)
}
return errs
}
@ -454,84 +456,6 @@ func printSwarmInfo(output io.Writer, info system.Info) {
}
}
func printServerWarnings(stdErr io.Writer, info *dockerInfo) {
if versions.LessThan(info.ClientInfo.APIVersion, "1.42") {
printSecurityOptionsWarnings(stdErr, *info.Info)
}
if len(info.Warnings) > 0 {
fprintln(stdErr, strings.Join(info.Warnings, "\n"))
return
}
// daemon didn't return warnings. Fallback to old behavior
printServerWarningsLegacy(stdErr, *info.Info)
}
// printSecurityOptionsWarnings prints warnings based on the security options
// returned by the daemon.
//
// Deprecated: warnings are now generated by the daemon, and returned in
// info.Warnings. This function is used to provide backward compatibility with
// daemons that do not provide these warnings. No new warnings should be added
// here.
func printSecurityOptionsWarnings(stdErr io.Writer, info system.Info) {
if info.OSType == "windows" {
return
}
kvs, _ := system.DecodeSecurityOptions(info.SecurityOptions)
for _, so := range kvs {
if so.Name != "seccomp" {
continue
}
for _, o := range so.Options {
if o.Key == "profile" && o.Value != "default" && o.Value != "builtin" {
_, _ = fmt.Fprintln(stdErr, "WARNING: You're not using the default seccomp profile")
}
}
}
}
// printServerWarningsLegacy generates warnings based on information returned by the daemon.
//
// Deprecated: warnings are now generated by the daemon, and returned in
// info.Warnings. This function is used to provide backward compatibility with
// daemons that do not provide these warnings. No new warnings should be added
// here.
func printServerWarningsLegacy(stdErr io.Writer, info system.Info) {
if info.OSType == "windows" {
return
}
if !info.MemoryLimit {
fprintln(stdErr, "WARNING: No memory limit support")
}
if !info.SwapLimit {
fprintln(stdErr, "WARNING: No swap limit support")
}
if !info.OomKillDisable && info.CgroupVersion != "2" {
fprintln(stdErr, "WARNING: No oom kill disable support")
}
if !info.CPUCfsQuota {
fprintln(stdErr, "WARNING: No cpu cfs quota support")
}
if !info.CPUCfsPeriod {
fprintln(stdErr, "WARNING: No cpu cfs period support")
}
if !info.CPUShares {
fprintln(stdErr, "WARNING: No cpu shares support")
}
if !info.CPUSet {
fprintln(stdErr, "WARNING: No cpuset support")
}
if !info.IPv4Forwarding {
fprintln(stdErr, "WARNING: IPv4 forwarding is disabled")
}
if !info.BridgeNfIptables {
fprintln(stdErr, "WARNING: bridge-nf-call-iptables is disabled")
}
if !info.BridgeNfIP6tables {
fprintln(stdErr, "WARNING: bridge-nf-call-ip6tables is disabled")
}
}
func formatInfo(output io.Writer, info dockerInfo, format string) error {
if format == formatter.JSONFormatKey {
format = formatter.JSONFormat

View File

@ -333,23 +333,6 @@ func TestPrettyPrintInfo(t *testing.T) {
prettyGolden: "docker-info-with-swarm",
jsonGolden: "docker-info-with-swarm",
},
{
doc: "info with legacy warnings",
dockerInfo: dockerInfo{
Info: &infoWithWarningsLinux,
ClientInfo: &clientInfo{
clientVersion: clientVersion{
Platform: &platformInfo{Name: "Docker Engine - Community"},
Version: "24.0.0",
Context: "default",
},
Debug: true,
},
},
prettyGolden: "docker-info-no-swarm",
warningsGolden: "docker-info-warnings",
jsonGolden: "docker-info-legacy-warnings",
},
{
doc: "info with daemon warnings",
dockerInfo: dockerInfo{

View File

@ -74,8 +74,10 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
if options.pruneVolumes && options.filter.Value().Contains("until") {
return fmt.Errorf(`ERROR: The "until" filter is not supported with "--volumes"`)
}
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), confirmationMessage(dockerCli, options)) {
return nil
if !options.force {
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), confirmationMessage(dockerCli, options)); !r || err != nil {
return err
}
}
pruneFuncs := []func(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error){
container.RunPrune,

View File

@ -1,10 +1,15 @@
package system
import (
"context"
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
@ -49,3 +54,23 @@ func TestPrunePromptFilters(t *testing.T) {
Are you sure you want to continue? [y/N] `
assert.Check(t, is.Equal(expected, cli.OutBuffer().String()))
}
func TestSystemPrunePromptTermination(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
containerPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) {
return types.ContainersPruneReport{}, errors.New("fakeClient containerPruneFunc should not be called")
},
networkPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (types.NetworksPruneReport, error) {
return types.NetworksPruneReport{}, errors.New("fakeClient networkPruneFunc should not be called")
},
})
cmd := newPruneCommand(cli)
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
t.Helper()
assert.ErrorIs(t, err, command.ErrPromptTerminated)
})
}

View File

@ -1 +0,0 @@
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":false,"SwapLimit":false,"CpuCfsPeriod":false,"CpuCfsQuota":false,"CPUShares":false,"CPUSet":false,"PidsLimit":false,"IPv4Forwarding":false,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"Debug":true,"NFd":33,"OomKillDisable":false,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}}

View File

@ -11,6 +11,7 @@ import (
"github.com/docker/cli/internal/test"
notaryfake "github.com/docker/cli/internal/test/notary"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/system"
apiclient "github.com/docker/docker/client"
"github.com/theupdateframework/notary"
@ -36,7 +37,7 @@ func (c *fakeClient) ImageInspectWithRaw(context.Context, string) (types.ImageIn
return types.ImageInspect{}, []byte{}, nil
}
func (c *fakeClient) ImagePush(context.Context, string, types.ImagePushOptions) (io.ReadCloser, error) {
func (c *fakeClient) ImagePush(context.Context, string, image.PushOptions) (io.ReadCloser, error) {
return &utils.NoopCloser{Reader: bytes.NewBuffer([]byte{})}, nil
}

View File

@ -3,7 +3,6 @@ package trust
import (
"context"
"fmt"
"os"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
@ -44,7 +43,11 @@ func revokeTrust(ctx context.Context, dockerCLI command.Cli, remote string, opti
return fmt.Errorf("cannot use a digest reference for IMAGE:TAG")
}
if imgRefAndAuth.Tag() == "" && !options.forceYes {
deleteRemote := command.PromptForConfirmation(os.Stdin, dockerCLI.Out(), fmt.Sprintf("Please confirm you would like to delete all signature data for %s?", remote))
deleteRemote, err := command.PromptForConfirmation(ctx, dockerCLI.In(), dockerCLI.Out(), fmt.Sprintf("Please confirm you would like to delete all signature data for %s?", remote))
if err != nil {
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
return errors.Wrap(err, "aborting action")
}
if !deleteRemote {
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
return nil

View File

@ -1,9 +1,11 @@
package trust
import (
"context"
"io"
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary"
@ -12,6 +14,7 @@ import (
"github.com/theupdateframework/notary/trustpinning"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
)
func TestTrustRevokeCommandErrors(t *testing.T) {
@ -148,3 +151,18 @@ func TestGetSignableRolesForTargetAndRemoveError(t *testing.T) {
err = getSignableRolesForTargetAndRemove(target, notaryRepo)
assert.Error(t, err, "client is offline")
}
func TestRevokeTrustPromptTermination(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{})
cmd := newRevokeCommand(cli)
cmd.SetArgs([]string{"example/trust-demo"})
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
t.Helper()
assert.ErrorIs(t, err, command.ErrPromptTerminated)
})
assert.Equal(t, cli.ErrBuffer().String(), "")
golden.Assert(t, cli.OutBuffer().String(), "trust-revoke-prompt-termination.golden")
}

View File

@ -12,7 +12,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/trust"
"github.com/docker/docker/api/types"
imagetypes "github.com/docker/docker/api/types/image"
registrytypes "github.com/docker/docker/api/types/registry"
apiclient "github.com/docker/docker/client"
"github.com/pkg/errors"
@ -98,7 +98,7 @@ func runSignImage(ctx context.Context, dockerCLI command.Cli, options signOption
if err != nil {
return err
}
options := types.ImagePushOptions{
options := imagetypes.PushOptions{
RegistryAuth: encodedAuth,
PrivilegeFunc: requestPrivilege,
}

View File

@ -3,7 +3,6 @@ package trust
import (
"context"
"fmt"
"os"
"strings"
"github.com/docker/cli/cli"
@ -76,6 +75,22 @@ func isLastSignerForReleases(roleWithSig data.Role, allRoles []client.RoleWithSi
return counter < releasesRoleWithSigs.Threshold, nil
}
func maybePromptForSignerRemoval(ctx context.Context, dockerCLI command.Cli, repoName, signerName string, isLastSigner, forceYes bool) (bool, error) {
if isLastSigner && !forceYes {
message := fmt.Sprintf("The signer \"%s\" signed the last released version of %s. "+
"Removing this signer will make %s unpullable. "+
"Are you sure you want to continue?",
signerName, repoName, repoName,
)
removeSigner, err := command.PromptForConfirmation(ctx, dockerCLI.In(), dockerCLI.Out(), message)
if err != nil {
return false, err
}
return removeSigner, nil
}
return false, nil
}
// removeSingleSigner attempts to remove a single signer and returns whether signer removal happened.
// The signer not being removed doesn't necessarily raise an error e.g. user choosing "No" when prompted for confirmation.
func removeSingleSigner(ctx context.Context, dockerCLI command.Cli, repoName, signerName string, forceYes bool) (bool, error) {
@ -110,28 +125,26 @@ func removeSingleSigner(ctx context.Context, dockerCLI command.Cli, repoName, si
if err != nil {
return false, err
}
if ok, err := isLastSignerForReleases(role, allRoles); ok && !forceYes {
removeSigner := command.PromptForConfirmation(os.Stdin, dockerCLI.Out(), fmt.Sprintf("The signer \"%s\" signed the last released version of %s. "+
"Removing this signer will make %s unpullable. "+
"Are you sure you want to continue?",
signerName, repoName, repoName,
))
if !removeSigner {
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
return false, nil
}
} else if err != nil {
return false, err
}
if err = notaryRepo.RemoveDelegationKeys(releasesRoleTUFName, role.KeyIDs); err != nil {
return false, err
}
if err = notaryRepo.RemoveDelegationRole(signerDelegation); err != nil {
isLastSigner, err := isLastSignerForReleases(role, allRoles)
if err != nil {
return false, err
}
if err = notaryRepo.Publish(); err != nil {
ok, err := maybePromptForSignerRemoval(ctx, dockerCLI, repoName, signerName, isLastSigner, forceYes)
if err != nil || !ok {
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
return false, err
}
if err := notaryRepo.RemoveDelegationKeys(releasesRoleTUFName, role.KeyIDs); err != nil {
return false, err
}
if err := notaryRepo.RemoveDelegationRole(signerDelegation); err != nil {
return false, err
}
if err := notaryRepo.Publish(); err != nil {
return false, err
}

View File

@ -111,7 +111,8 @@ func TestIsLastSignerForReleases(t *testing.T) {
releaserole.Name = releasesRoleTUFName
releaserole.Threshold = 1
allrole := []client.RoleWithSignatures{releaserole}
lastsigner, _ := isLastSignerForReleases(role, allrole)
lastsigner, err := isLastSignerForReleases(role, allrole)
assert.Error(t, err, "all signed tags are currently revoked, use docker trust sign to fix")
assert.Check(t, is.Equal(false, lastsigner))
role.KeyIDs = []string{"deadbeef"}
@ -120,13 +121,15 @@ func TestIsLastSignerForReleases(t *testing.T) {
releaserole.Signatures = []data.Signature{sig}
releaserole.Threshold = 1
allrole = []client.RoleWithSignatures{releaserole}
lastsigner, _ = isLastSignerForReleases(role, allrole)
lastsigner, err = isLastSignerForReleases(role, allrole)
assert.NilError(t, err)
assert.Check(t, is.Equal(true, lastsigner))
sig.KeyID = "8badf00d"
releaserole.Signatures = []data.Signature{sig}
releaserole.Threshold = 1
allrole = []client.RoleWithSignatures{releaserole}
lastsigner, _ = isLastSignerForReleases(role, allrole)
lastsigner, err = isLastSignerForReleases(role, allrole)
assert.NilError(t, err)
assert.Check(t, is.Equal(false, lastsigner))
}

View File

@ -0,0 +1,2 @@
Please confirm you would like to delete all signature data for example/trust-demo? [y/N]
Aborting action.

View File

@ -5,12 +5,15 @@ package command
import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"syscall"
"github.com/docker/cli/cli/streams"
"github.com/docker/docker/api/types/filters"
@ -72,12 +75,21 @@ func PrettyPrint(i any) string {
}
}
// PromptForConfirmation requests and checks confirmation from user.
// This will display the provided message followed by ' [y/N] '. If
// the user input 'y' or 'Y' it returns true other false. If no
// message is provided "Are you sure you want to proceed? [y/N] "
// will be used instead.
func PromptForConfirmation(ins io.Reader, outs io.Writer, message string) bool {
type PromptError error
var ErrPromptTerminated = PromptError(errors.New("prompt terminated"))
// PromptForConfirmation requests and checks confirmation from the user.
// This will display the provided message followed by ' [y/N] '. If the user
// input 'y' or 'Y' it returns true otherwise false. If no message is provided,
// "Are you sure you want to proceed? [y/N] " will be used instead.
//
// If the user terminates the CLI with SIGINT or SIGTERM while the prompt is
// active, the prompt will return false with an ErrPromptTerminated error.
// When the prompt returns an error, the caller should propagate the error up
// the stack and close the io.Reader used for the prompt which will prevent the
// background goroutine from blocking indefinitely.
func PromptForConfirmation(ctx context.Context, ins io.Reader, outs io.Writer, message string) (bool, error) {
if message == "" {
message = "Are you sure you want to proceed?"
}
@ -90,9 +102,31 @@ func PromptForConfirmation(ins io.Reader, outs io.Writer, message string) bool {
ins = streams.NewIn(os.Stdin)
}
reader := bufio.NewReader(ins)
answer, _, _ := reader.ReadLine()
return strings.ToLower(string(answer)) == "y"
result := make(chan bool)
// Catch the termination signal and exit the prompt gracefully.
// The caller is responsible for properly handling the termination.
notifyCtx, notifyCancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
defer notifyCancel()
go func() {
var res bool
scanner := bufio.NewScanner(ins)
if scanner.Scan() {
answer := strings.TrimSpace(scanner.Text())
if strings.EqualFold(answer, "y") {
res = true
}
}
result <- res
}()
select {
case <-notifyCtx.Done():
return false, ErrPromptTerminated
case r := <-result:
return r, nil
}
}
// PruneFilters returns consolidated prune filters obtained from config.json and cli

View File

@ -1,36 +1,46 @@
package command
package command_test
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/test"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
)
func TestStringSliceReplaceAt(t *testing.T) {
out, ok := StringSliceReplaceAt([]string{"abc", "foo", "bar", "bax"}, []string{"foo", "bar"}, []string{"baz"}, -1)
out, ok := command.StringSliceReplaceAt([]string{"abc", "foo", "bar", "bax"}, []string{"foo", "bar"}, []string{"baz"}, -1)
assert.Assert(t, ok)
assert.DeepEqual(t, []string{"abc", "baz", "bax"}, out)
out, ok = StringSliceReplaceAt([]string{"foo"}, []string{"foo", "bar"}, []string{"baz"}, -1)
out, ok = command.StringSliceReplaceAt([]string{"foo"}, []string{"foo", "bar"}, []string{"baz"}, -1)
assert.Assert(t, !ok)
assert.DeepEqual(t, []string{"foo"}, out)
out, ok = StringSliceReplaceAt([]string{"abc", "foo", "bar", "bax"}, []string{"foo", "bar"}, []string{"baz"}, 0)
out, ok = command.StringSliceReplaceAt([]string{"abc", "foo", "bar", "bax"}, []string{"foo", "bar"}, []string{"baz"}, 0)
assert.Assert(t, !ok)
assert.DeepEqual(t, []string{"abc", "foo", "bar", "bax"}, out)
out, ok = StringSliceReplaceAt([]string{"foo", "bar", "bax"}, []string{"foo", "bar"}, []string{"baz"}, 0)
out, ok = command.StringSliceReplaceAt([]string{"foo", "bar", "bax"}, []string{"foo", "bar"}, []string{"baz"}, 0)
assert.Assert(t, ok)
assert.DeepEqual(t, []string{"baz", "bax"}, out)
out, ok = StringSliceReplaceAt([]string{"abc", "foo", "bar", "baz"}, []string{"foo", "bar"}, nil, -1)
out, ok = command.StringSliceReplaceAt([]string{"abc", "foo", "bar", "baz"}, []string{"foo", "bar"}, nil, -1)
assert.Assert(t, ok)
assert.DeepEqual(t, []string{"abc", "baz"}, out)
out, ok = StringSliceReplaceAt([]string{"foo"}, nil, []string{"baz"}, -1)
out, ok = command.StringSliceReplaceAt([]string{"foo"}, nil, []string{"baz"}, -1)
assert.Assert(t, !ok)
assert.DeepEqual(t, []string{"foo"}, out)
}
@ -59,7 +69,7 @@ func TestValidateOutputPath(t *testing.T) {
for _, testcase := range testcases {
t.Run(testcase.path, func(t *testing.T) {
err := ValidateOutputPath(testcase.path)
err := command.ValidateOutputPath(testcase.path)
if testcase.err == nil {
assert.NilError(t, err)
} else {
@ -68,3 +78,177 @@ func TestValidateOutputPath(t *testing.T) {
})
}
}
func TestPromptForConfirmation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
type promptResult struct {
result bool
err error
}
buf := new(bytes.Buffer)
bufioWriter := bufio.NewWriter(buf)
var (
promptWriter *io.PipeWriter
promptReader *io.PipeReader
)
defer func() {
if promptWriter != nil {
promptWriter.Close()
}
if promptReader != nil {
promptReader.Close()
}
}()
for _, tc := range []struct {
desc string
f func(*testing.T, context.Context, chan promptResult)
}{
{"SIGINT", func(t *testing.T, ctx context.Context, c chan promptResult) {
t.Helper()
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
select {
case <-ctx.Done():
t.Fatal("PromptForConfirmation did not return after SIGINT")
case r := <-c:
assert.Check(t, !r.result)
assert.ErrorContains(t, r.err, "prompt terminated")
}
}},
{"no", func(t *testing.T, ctx context.Context, c chan promptResult) {
t.Helper()
_, err := fmt.Fprint(promptWriter, "n\n")
assert.NilError(t, err)
select {
case <-ctx.Done():
t.Fatal("PromptForConfirmation did not return after user input `n`")
case r := <-c:
assert.Check(t, !r.result)
assert.NilError(t, r.err)
}
}},
{"yes", func(t *testing.T, ctx context.Context, c chan promptResult) {
t.Helper()
_, err := fmt.Fprint(promptWriter, "y\n")
assert.NilError(t, err)
select {
case <-ctx.Done():
t.Fatal("PromptForConfirmation did not return after user input `y`")
case r := <-c:
assert.Check(t, r.result)
assert.NilError(t, r.err)
}
}},
{"any", func(t *testing.T, ctx context.Context, c chan promptResult) {
t.Helper()
_, err := fmt.Fprint(promptWriter, "a\n")
assert.NilError(t, err)
select {
case <-ctx.Done():
t.Fatal("PromptForConfirmation did not return after user input `a`")
case r := <-c:
assert.Check(t, !r.result)
assert.NilError(t, r.err)
}
}},
{"with space", func(t *testing.T, ctx context.Context, c chan promptResult) {
t.Helper()
_, err := fmt.Fprint(promptWriter, " y\n")
assert.NilError(t, err)
select {
case <-ctx.Done():
t.Fatal("PromptForConfirmation did not return after user input ` y`")
case r := <-c:
assert.Check(t, r.result)
assert.NilError(t, r.err)
}
}},
{"reader closed", func(t *testing.T, ctx context.Context, c chan promptResult) {
t.Helper()
assert.NilError(t, promptReader.Close())
select {
case <-ctx.Done():
t.Fatal("PromptForConfirmation did not return after promptReader was closed")
case r := <-c:
assert.Check(t, !r.result)
assert.NilError(t, r.err)
}
}},
} {
t.Run("case="+tc.desc, func(t *testing.T) {
buf.Reset()
promptReader, promptWriter = io.Pipe()
wroteHook := make(chan struct{}, 1)
defer close(wroteHook)
promptOut := test.NewWriterWithHook(bufioWriter, func(p []byte) {
wroteHook <- struct{}{}
})
result := make(chan promptResult, 1)
defer close(result)
go func() {
r, err := command.PromptForConfirmation(ctx, promptReader, promptOut, "")
result <- promptResult{r, err}
}()
// wait for the Prompt to write to the buffer
pollForPromptOutput(ctx, t, wroteHook)
drainChannel(ctx, wroteHook)
assert.NilError(t, bufioWriter.Flush())
assert.Equal(t, strings.TrimSpace(buf.String()), "Are you sure you want to proceed? [y/N]")
resultCtx, resultCancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer resultCancel()
tc.f(t, resultCtx, result)
})
}
}
func drainChannel(ctx context.Context, ch <-chan struct{}) {
go func() {
for {
select {
case <-ctx.Done():
return
case <-ch:
}
}
}()
}
func pollForPromptOutput(ctx context.Context, t *testing.T, wroteHook <-chan struct{}) {
t.Helper()
ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
for {
select {
case <-ctx.Done():
t.Fatal("Prompt output was not written to before ctx was cancelled")
return
case <-wroteHook:
return
}
}
}

View File

@ -32,6 +32,10 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCli, options)
if err != nil {
if errdefs.IsCancelled(err) {
fmt.Fprintln(dockerCli.Out(), output)
return nil
}
return err
}
if output != "" {
@ -76,8 +80,10 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
// API < v1.42 removes all volumes (anonymous and named) by default.
warning = allVolumesWarning
}
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
return 0, "", nil
if !options.force {
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
return 0, "", errdefs.Cancelled(errors.New("user cancelled operation"))
}
}
report, err := dockerCli.Client().VolumesPrune(ctx, pruneFilters)

View File

@ -1,6 +1,7 @@
package volume
import (
"context"
"fmt"
"io"
"runtime"
@ -73,13 +74,15 @@ func TestVolumePruneSuccess(t *testing.T) {
testCases := []struct {
name string
args []string
input string
volumePruneFunc func(args filters.Args) (types.VolumesPruneReport, error)
}{
{
name: "all",
args: []string{"--all"},
name: "all",
args: []string{"--all"},
input: "y",
volumePruneFunc: func(pruneFilter filters.Args) (types.VolumesPruneReport, error) {
assert.Check(t, is.Equal([]string{"true"}, pruneFilter.Get("all")))
assert.Check(t, is.DeepEqual([]string{"true"}, pruneFilter.Get("all")))
return types.VolumesPruneReport{}, nil
},
},
@ -91,10 +94,11 @@ func TestVolumePruneSuccess(t *testing.T) {
},
},
{
name: "label-filter",
args: []string{"--filter", "label=foobar"},
name: "label-filter",
args: []string{"--filter", "label=foobar"},
input: "y",
volumePruneFunc: func(pruneFilter filters.Args) (types.VolumesPruneReport, error) {
assert.Check(t, is.Equal([]string{"foobar"}, pruneFilter.Get("label")))
assert.Check(t, is.DeepEqual([]string{"foobar"}, pruneFilter.Get("label")))
return types.VolumesPruneReport{}, nil
},
},
@ -104,6 +108,9 @@ func TestVolumePruneSuccess(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{volumePruneFunc: tc.volumePruneFunc})
cmd := NewPruneCommand(cli)
if tc.input != "" {
cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(tc.input))))
}
cmd.SetOut(io.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
@ -177,3 +184,19 @@ func simplePruneFunc(filters.Args) (types.VolumesPruneReport, error) {
SpaceReclaimed: 2000,
}, nil
}
func TestVolumePrunePromptTerminate(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
volumePruneFunc: func(filter filters.Args) (types.VolumesPruneReport, error) {
return types.VolumesPruneReport{}, errors.New("fakeClient volumePruneFunc should not be called")
},
})
cmd := NewPruneCommand(cli)
test.TerminatePrompt(ctx, t, cmd, cli, nil)
golden.Assert(t, cli.OutBuffer().String(), "volume-prune-terminate.golden")
}

View File

@ -1,2 +1,2 @@
WARNING! This will remove anonymous local volumes not used by at least one container.
Are you sure you want to continue? [y/N] Total reclaimed space: 0B
Are you sure you want to continue? [y/N]

View File

@ -0,0 +1,2 @@
WARNING! This will remove anonymous local volumes not used by at least one container.
Are you sure you want to continue? [y/N]

View File

@ -328,7 +328,7 @@ func createTransformHook(additionalTransformers ...Transformer) mapstructure.Dec
reflect.TypeOf(types.MappingWithEquals{}): transformMappingOrListFunc("=", true),
reflect.TypeOf(types.Labels{}): transformMappingOrListFunc("=", false),
reflect.TypeOf(types.MappingWithColon{}): transformMappingOrListFunc(":", false),
reflect.TypeOf(types.HostsList{}): transformListOrMappingFunc(":", false),
reflect.TypeOf(types.HostsList{}): transformHostsList,
reflect.TypeOf(types.ServiceVolumeConfig{}): transformServiceVolumeConfig,
reflect.TypeOf(types.BuildConfig{}): transformBuildConfig,
reflect.TypeOf(types.Duration(0)): transformStringToDuration,
@ -808,28 +808,58 @@ var transformStringList TransformerFunc = func(data any) (any, error) {
}
}
func transformMappingOrListFunc(sep string, allowNil bool) TransformerFunc {
return func(data any) (any, error) {
return transformMappingOrList(data, sep, allowNil), nil
var transformHostsList TransformerFunc = func(data any) (any, error) {
hl := transformListOrMapping(data, ":", false, []string{"=", ":"})
// Remove brackets from IP addresses if present (for example "[::1]" -> "::1").
result := make([]string, 0, len(hl))
for _, hip := range hl {
host, ip, _ := strings.Cut(hip, ":")
if len(ip) > 2 && ip[0] == '[' && ip[len(ip)-1] == ']' {
ip = ip[1 : len(ip)-1]
}
result = append(result, fmt.Sprintf("%s:%s", host, ip))
}
return result, nil
}
func transformListOrMappingFunc(sep string, allowNil bool) TransformerFunc {
return func(data any) (any, error) {
return transformListOrMapping(data, sep, allowNil), nil
}
}
func transformListOrMapping(listOrMapping any, sep string, allowNil bool) any {
// transformListOrMapping transforms pairs of strings that may be represented as
// a map, or a list of '=' or ':' separated strings, into a list of ':' separated
// strings.
func transformListOrMapping(listOrMapping any, sep string, allowNil bool, allowSeps []string) []string {
switch value := listOrMapping.(type) {
case map[string]any:
return toStringList(value, sep, allowNil)
case []any:
return listOrMapping
result := make([]string, 0, len(value))
for _, entry := range value {
for i, allowSep := range allowSeps {
entry := fmt.Sprint(entry)
k, v, ok := strings.Cut(entry, allowSep)
if ok {
// Entry uses this allowed separator. Add it to the result, using
// sep as a separator.
result = append(result, fmt.Sprintf("%s%s%s", k, sep, v))
break
} else if i == len(allowSeps)-1 {
// No more separators to try, keep the entry if allowNil.
if allowNil {
result = append(result, k)
}
}
}
}
return result
}
panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping))
}
func transformMappingOrListFunc(sep string, allowNil bool) TransformerFunc {
return func(data any) (any, error) {
return transformMappingOrList(data, sep, allowNil), nil
}
}
func transformMappingOrList(mappingOrList any, sep string, allowNil bool) any {
switch values := mappingOrList.(type) {
case map[string]any:

View File

@ -1302,12 +1302,14 @@ services:
extra_hosts:
"zulu": "162.242.195.82"
"alpha": "50.31.209.229"
"beta": "[fd20:f8a7:6e5b::2]"
"host.docker.internal": "host-gateway"
`)
assert.NilError(t, err)
expected := types.HostsList{
"alpha:50.31.209.229",
"beta:fd20:f8a7:6e5b::2",
"host.docker.internal:host-gateway",
"zulu:162.242.195.82",
}
@ -1324,16 +1326,25 @@ services:
image: busybox
extra_hosts:
- "zulu:162.242.195.82"
- "whiskey=162.242.195.83"
- "alpha:50.31.209.229"
- "zulu:ff02::1"
- "host.docker.internal:host-gateway"
- "whiskey=ff02::2"
- "foxtrot=[ff02::3]"
- "bravo:[ff02::4]"
- "host.docker.internal=host-gateway"
- "noaddress"
`)
assert.NilError(t, err)
expected := types.HostsList{
"zulu:162.242.195.82",
"whiskey:162.242.195.83",
"alpha:50.31.209.229",
"zulu:ff02::1",
"whiskey:ff02::2",
"foxtrot:ff02::3",
"bravo:ff02::4",
"host.docker.internal:host-gateway",
}

View File

@ -17,9 +17,7 @@ import (
// For https://github.com/docker/cli/pull/1014#issuecomment-409308139
func TestEOFWithError(t *testing.T) {
ctx := context.TODO()
cmd := "sh"
args := []string{"-c", "echo hello; echo some error >&2; exit 42"}
c, err := New(ctx, cmd, args...)
c, err := New(ctx, "sh", "-c", "echo hello; echo some error >&2; exit 42")
assert.NilError(t, err)
b := make([]byte, 32)
n, err := c.Read(b)
@ -33,9 +31,7 @@ func TestEOFWithError(t *testing.T) {
func TestEOFWithoutError(t *testing.T) {
ctx := context.TODO()
cmd := "sh"
args := []string{"-c", "echo hello; echo some debug log >&2; exit 0"}
c, err := New(ctx, cmd, args...)
c, err := New(ctx, "sh", "-c", "echo hello; echo some debug log >&2; exit 0")
assert.NilError(t, err)
b := make([]byte, 32)
n, err := c.Read(b)
@ -47,14 +43,12 @@ func TestEOFWithoutError(t *testing.T) {
}
func TestCloseRunningCommand(t *testing.T) {
cmd := "sh"
args := []string{"-c", "while true; sleep 1; done"}
ctx := context.TODO()
done := make(chan struct{})
defer close(done)
go func() {
c, err := New(context.TODO(), cmd, args...)
c, err := New(ctx, "sh", "-c", "while true; sleep 1; done")
assert.NilError(t, err)
cmdConn := c.(*commandConn)
assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid))
@ -79,12 +73,10 @@ func TestCloseRunningCommand(t *testing.T) {
}
func TestCloseTwice(t *testing.T) {
cmd := "sh"
args := []string{"-c", "echo hello; sleep 1; exit 0"}
ctx := context.TODO()
done := make(chan struct{})
go func() {
c, err := New(context.TODO(), cmd, args...)
c, err := New(ctx, "sh", "-c", "echo hello; sleep 1; exit 0")
assert.NilError(t, err)
cmdConn := c.(*commandConn)
assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid))
@ -113,12 +105,10 @@ func TestCloseTwice(t *testing.T) {
}
func TestEOFTimeout(t *testing.T) {
cmd := "sh"
args := []string{"-c", "sleep 20"}
ctx := context.TODO()
done := make(chan struct{})
go func() {
c, err := New(context.TODO(), cmd, args...)
c, err := New(ctx, "sh", "-c", "sleep 20")
assert.NilError(t, err)
cmdConn := c.(*commandConn)
assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid))
@ -154,10 +144,8 @@ func (mockStdoutEOF) Close() error {
}
func TestCloseWhileWriting(t *testing.T) {
cmd := "sh"
args := []string{"-c", "while true; sleep 1; done"}
c, err := New(context.TODO(), cmd, args...)
ctx := context.TODO()
c, err := New(ctx, "sh", "-c", "while true; sleep 1; done")
assert.NilError(t, err)
cmdConn := c.(*commandConn)
assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid))
@ -184,10 +172,8 @@ func TestCloseWhileWriting(t *testing.T) {
}
func TestCloseWhileReading(t *testing.T) {
cmd := "sh"
args := []string{"-c", "while true; sleep 1; done"}
c, err := New(context.TODO(), cmd, args...)
ctx := context.TODO()
c, err := New(ctx, "sh", "-c", "while true; sleep 1; done")
assert.NilError(t, err)
cmdConn := c.(*commandConn)
assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid))

View File

@ -69,7 +69,7 @@ func processBuilder(dockerCli command.Cli, cmd *cobra.Command, args, osargs []st
}
// is this a build that should be forwarded to the builder?
fwargs, fwosargs, forwarded := forwardBuilder(builderAlias, args, osargs)
fwargs, fwosargs, alias, forwarded := forwardBuilder(builderAlias, args, osargs)
if !forwarded {
return args, osargs, nil, nil
}
@ -116,10 +116,13 @@ func processBuilder(dockerCli command.Cli, cmd *cobra.Command, args, osargs []st
envs = append([]string{"BUILDX_BUILDER=" + dockerCli.CurrentContext()}, envs...)
}
// overwrite the command path for this plugin using the alias name.
cmd.Annotations[pluginmanager.CommandAnnotationPluginCommandPath] = fmt.Sprintf("%s %s", cmd.CommandPath(), alias)
return fwargs, fwosargs, envs, nil
}
func forwardBuilder(alias string, args, osargs []string) ([]string, []string, bool) {
func forwardBuilder(alias string, args, osargs []string) ([]string, []string, string, bool) {
aliases := [][2][]string{
{
{"builder"},
@ -137,10 +140,10 @@ func forwardBuilder(alias string, args, osargs []string) ([]string, []string, bo
for _, al := range aliases {
if fwargs, changed := command.StringSliceReplaceAt(args, al[0], al[1], 0); changed {
fwosargs, _ := command.StringSliceReplaceAt(osargs, al[0], al[1], -1)
return fwargs, fwosargs, true
return fwargs, fwosargs, al[0][0], true
}
}
return args, osargs, false
return args, osargs, "", false
}
// hasBuilderName checks if a builder name is defined in args or env vars

View File

@ -1146,6 +1146,7 @@ __docker_complete_plugin() {
local path=$1
local completionCommand="__completeNoDesc"
local resultArray=($path $completionCommand)
local current="$cur"
for value in "${words[@]:2}"; do
if [ -z "$value" ]; then
resultArray+=( "''" )

View File

@ -1,5 +1,5 @@
variable "GO_VERSION" {
default = "1.21.6"
default = "1.21.8"
}
variable "VERSION" {
default = ""
@ -186,6 +186,7 @@ target "bin-image-cross" {
"linux/arm64",
"linux/ppc64le",
"linux/s390x",
"windows/amd64"
"windows/amd64",
"windows/arm64"
]
}

View File

@ -14,13 +14,13 @@ PACKAGER_NAME ?=
DEV_DOCKER_IMAGE_NAME = docker-cli-dev$(IMAGE_TAG)
E2E_IMAGE_NAME = docker-cli-e2e
E2E_ENGINE_VERSION ?=
ENGINE_VERSION ?=
CACHE_VOLUME_NAME := docker-cli-dev-cache
ifeq ($(DOCKER_CLI_GO_BUILD_CACHE),y)
DOCKER_CLI_MOUNTS += -v "$(CACHE_VOLUME_NAME):/root/.cache/go-build"
endif
VERSION = $(shell cat VERSION)
ENVVARS = -e VERSION=$(VERSION) -e GITCOMMIT -e PLATFORM -e TESTFLAGS -e TESTDIRS -e GOOS -e GOARCH -e GOARM -e TEST_ENGINE_VERSION=$(E2E_ENGINE_VERSION)
ENVVARS = -e VERSION=$(VERSION) -e GITCOMMIT -e PLATFORM -e TESTFLAGS -e TESTDIRS -e GOOS -e GOARCH -e GOARM -e ENGINE_VERSION
# Some Dockerfiles use features that are only supported with BuildKit enabled
export DOCKER_BUILDKIT=1
@ -132,21 +132,21 @@ test-e2e: test-e2e-non-experimental test-e2e-experimental test-e2e-connhelper-ss
.PHONY: test-e2e-experimental
test-e2e-experimental: build-e2e-image # run experimental e2e tests
docker run --rm $(ENVVARS) -e DOCKERD_EXPERIMENTAL=1 -e TEST_ENGINE_VERSION=$(E2E_ENGINE_VERSION) \
docker run --rm $(ENVVARS) -e DOCKERD_EXPERIMENTAL=1 \
--mount type=bind,src=$(CURDIR)/build/coverage,dst=/tmp/coverage \
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
$(E2E_IMAGE_NAME)
.PHONY: test-e2e-non-experimental
test-e2e-non-experimental: build-e2e-image # run non-experimental e2e tests
docker run --rm $(ENVVARS) -e TEST_ENGINE_VERSION=$(E2E_ENGINE_VERSION) \
docker run --rm $(ENVVARS) \
--mount type=bind,src=$(CURDIR)/build/coverage,dst=/tmp/coverage \
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
$(E2E_IMAGE_NAME)
.PHONY: test-e2e-connhelper-ssh
test-e2e-connhelper-ssh: build-e2e-image # run experimental SSH-connection helper e2e tests
docker run --rm $(ENVVARS) -e DOCKERD_EXPERIMENTAL=1 -e TEST_ENGINE_VERSION=$(E2E_ENGINE_VERSION) -e TEST_CONNHELPER=ssh \
docker run --rm $(ENVVARS) -e DOCKERD_EXPERIMENTAL=1 -e TEST_CONNHELPER=ssh \
--mount type=bind,src=$(CURDIR)/build/coverage,dst=/tmp/coverage \
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
$(E2E_IMAGE_NAME)

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