Compare commits

..

70 Commits

Author SHA1 Message Date
3ab4256958 Merge pull request #5374 from vvoland/vendor-docker
Some checks failed
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, 26.1, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 26.1, experimental) (push) Has been cancelled
e2e / e2e (alpine, 26.1, non-experimental) (push) Has been cancelled
e2e / e2e (alpine, 27, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 27, experimental) (push) Has been cancelled
e2e / e2e (alpine, 27, 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, 26.1, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 26.1, experimental) (push) Has been cancelled
e2e / e2e (debian, 26.1, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 27, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 27, experimental) (push) Has been cancelled
e2e / e2e (debian, 27, non-experimental) (push) Has been cancelled
test / ctn (push) Has been cancelled
test / host (macos-13) (push) Has been cancelled
test / host (macos-14) (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
[27.x backport] vendor: github.com/docker/docker 3ab5c7d0036c (v27.2.0-dev)
2024-08-27 16:08:11 +02:00
88a49df297 vendor: github.com/docker/docker 3ab5c7d0036c (v27.2.0-dev)
full diff: b27de4ef16...3ab5c7d003

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

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

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

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

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

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

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

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

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

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

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

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

added test, also added reflect for the DeepEqual comparison

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

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

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

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

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit fcfdd7b91f)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-08-16 10:09:38 +01:00
3eaf30278f docs: update link to moved build context doc
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit 2dd4eb06ae)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-08-13 12:17:58 +02:00
d01f264bcc Merge pull request #5333 from thaJeztah/27.x_bump_engine
Some checks failed
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, 26.1, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 26.1, experimental) (push) Has been cancelled
e2e / e2e (alpine, 26.1, non-experimental) (push) Has been cancelled
e2e / e2e (alpine, 27, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 27, experimental) (push) Has been cancelled
e2e / e2e (alpine, 27, 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, 26.1, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 26.1, experimental) (push) Has been cancelled
e2e / e2e (debian, 26.1, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 27, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 27, experimental) (push) Has been cancelled
e2e / e2e (debian, 27, non-experimental) (push) Has been cancelled
test / ctn (push) Has been cancelled
test / host (macos-13) (push) Has been cancelled
test / host (macos-14) (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
[27.x] vendor: github.com/docker/docker f9522e5e96c3 (v27.1.2-dev) (removes containerd dependency)
2024-08-12 13:34:32 +02:00
65dec14ac0 vendor: github.com/docker/docker f9522e5e96c3 (v27.1.2-dev)
Removes dependency on containerd, as the userns package was migrated
to the github.com/moby/sys/userns module.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Also adds a regression test.

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

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

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

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

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

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

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

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

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 401048b9cb)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-23 13:37:29 +02:00
195 changed files with 13547 additions and 630 deletions

View File

@ -1,5 +1,14 @@
name: build
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

View File

@ -1,5 +1,14 @@
name: codeql
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
on:
push:
branches:

View File

@ -1,5 +1,14 @@
name: e2e
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

View File

@ -1,5 +1,14 @@
name: test
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@ -65,7 +74,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.21.12
go-version: 1.21.13
-
name: Test
run: |

View File

@ -1,5 +1,14 @@
name: validate-pr
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
on:
pull_request:
types: [opened, edited, labeled, unlabeled]

View File

@ -1,5 +1,14 @@
name: validate
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

View File

@ -4,7 +4,7 @@ ARG BASE_VARIANT=alpine
ARG ALPINE_VERSION=3.20
ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.21.12
ARG GO_VERSION=1.21.13
ARG XX_VERSION=1.4.0
ARG GOVERSIONINFO_VERSION=v1.3.0
ARG GOTESTSUM_VERSION=v1.10.0

View File

@ -95,6 +95,9 @@ func (pl *PluginServer) Addr() net.Addr {
//
// The error value is that of the underlying [net.Listner.Close] call.
func (pl *PluginServer) Close() error {
if pl == nil {
return nil
}
logrus.Trace("Closing plugin server")
// Close connections first to ensure the connections get io.EOF instead
// of a connection reset.

View File

@ -117,6 +117,18 @@ func TestPluginServer(t *testing.T) {
assert.NilError(t, err, "failed to dial returned server")
checkDirNoNewPluginServer(t)
})
t.Run("does not panic on Close if server is nil", func(t *testing.T) {
var srv *PluginServer
defer func() {
if r := recover(); r != nil {
t.Errorf("panicked on Close")
}
}()
err := srv.Close()
assert.NilError(t, err)
})
}
func checkDirNoNewPluginServer(t *testing.T) {

View File

@ -73,7 +73,8 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
apiClient := dockerCLI.Client()
// request channel to wait for client
resultC, errC := apiClient.ContainerWait(ctx, containerID, "")
waitCtx := context.WithoutCancel(ctx)
resultC, errC := apiClient.ContainerWait(waitCtx, containerID, "")
c, err := inspectContainerAndCheckState(ctx, apiClient, containerID)
if err != nil {
@ -146,7 +147,8 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
detachKeys: options.DetachKeys,
}
if err := streamer.stream(ctx); err != nil {
// if the context was canceled, this was likely intentional and we shouldn't return an error
if err := streamer.stream(ctx); err != nil && !errors.Is(err, context.Canceled) {
return err
}
@ -163,6 +165,9 @@ func getExitStatus(errC <-chan error, resultC <-chan container.WaitResponse) err
return cli.StatusError{StatusCode: int(result.StatusCode)}
}
case err := <-errC:
if errors.Is(err, context.Canceled) {
return nil
}
return err
}

View File

@ -1,6 +1,7 @@
package container
import (
"context"
"io"
"testing"
@ -81,11 +82,7 @@ func TestNewAttachCommandErrors(t *testing.T) {
}
func TestGetExitStatus(t *testing.T) {
var (
expectedErr = errors.New("unexpected error")
errC = make(chan error, 1)
resultC = make(chan container.WaitResponse, 1)
)
expectedErr := errors.New("unexpected error")
testcases := []struct {
result *container.WaitResponse
@ -113,16 +110,24 @@ func TestGetExitStatus(t *testing.T) {
},
expectedError: cli.StatusError{StatusCode: 15},
},
{
err: context.Canceled,
expectedError: nil,
},
}
for _, testcase := range testcases {
errC := make(chan error, 1)
resultC := make(chan container.WaitResponse, 1)
if testcase.err != nil {
errC <- testcase.err
}
if testcase.result != nil {
resultC <- *testcase.result
}
err := getExitStatus(errC, resultC)
if testcase.expectedError == nil {
assert.NilError(t, err)
} else {

View File

@ -5,7 +5,6 @@ import (
"errors"
"io"
"net"
"os/signal"
"syscall"
"testing"
"time"
@ -38,15 +37,85 @@ func TestRunLabel(t *testing.T) {
assert.NilError(t, cmd.Execute())
}
func TestRunAttachTermination(t *testing.T) {
func TestRunAttach(t *testing.T) {
p, tty, err := pty.Open()
assert.NilError(t, err)
defer func() {
_ = tty.Close()
_ = p.Close()
}()
var conn net.Conn
attachCh := make(chan struct{})
fakeCLI := test.NewFakeCli(&fakeClient{
createContainerFunc: func(_ *container.Config, _ *container.HostConfig, _ *network.NetworkingConfig, _ *specs.Platform, _ string) (container.CreateResponse, error) {
return container.CreateResponse{
ID: "id",
}, nil
},
containerAttachFunc: func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
server, client := net.Pipe()
conn = server
t.Cleanup(func() {
_ = server.Close()
})
attachCh <- struct{}{}
return types.NewHijackedResponse(client, types.MediaTypeRawStream), nil
},
waitFunc: func(_ string) (<-chan container.WaitResponse, <-chan error) {
responseChan := make(chan container.WaitResponse, 1)
errChan := make(chan error)
responseChan <- container.WaitResponse{
StatusCode: 33,
}
return responseChan, errChan
},
// use new (non-legacy) wait API
// see: 38591f20d07795aaef45d400df89ca12f29c603b
Version: "1.30",
}, func(fc *test.FakeCli) {
fc.SetOut(streams.NewOut(tty))
fc.SetIn(streams.NewIn(tty))
})
cmd := NewRunCommand(fakeCLI)
cmd.SetArgs([]string{"-it", "busybox"})
cmd.SilenceUsage = true
cmdErrC := make(chan error, 1)
go func() {
cmdErrC <- cmd.Execute()
}()
// run command should attempt to attach to the container
select {
case <-time.After(5 * time.Second):
t.Fatal("containerAttachFunc was not called before the 5 second timeout")
case <-attachCh:
}
// end stream from "container" so that we'll detach
conn.Close()
select {
case cmdErr := <-cmdErrC:
assert.Equal(t, cmdErr, cli.StatusError{
StatusCode: 33,
})
case <-time.After(2 * time.Second):
t.Fatal("cmd did not return within timeout")
}
}
func TestRunAttachTermination(t *testing.T) {
p, tty, err := pty.Open()
assert.NilError(t, err)
defer func() {
_ = tty.Close()
_ = p.Close()
}()
var conn net.Conn
killCh := make(chan struct{})
attachCh := make(chan struct{})
fakeCLI := test.NewFakeCli(&fakeClient{
@ -61,42 +130,62 @@ func TestRunAttachTermination(t *testing.T) {
},
containerAttachFunc: func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
server, client := net.Pipe()
conn = server
t.Cleanup(func() {
_ = server.Close()
})
attachCh <- struct{}{}
return types.NewHijackedResponse(client, types.MediaTypeRawStream), nil
},
Version: "1.36",
waitFunc: func(_ string) (<-chan container.WaitResponse, <-chan error) {
responseChan := make(chan container.WaitResponse, 1)
errChan := make(chan error)
responseChan <- container.WaitResponse{
StatusCode: 130,
}
return responseChan, errChan
},
// use new (non-legacy) wait API
// see: 38591f20d07795aaef45d400df89ca12f29c603b
Version: "1.30",
}, func(fc *test.FakeCli) {
fc.SetOut(streams.NewOut(tty))
fc.SetIn(streams.NewIn(tty))
})
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM)
defer cancel()
assert.Equal(t, fakeCLI.In().IsTerminal(), true)
assert.Equal(t, fakeCLI.Out().IsTerminal(), true)
cmd := NewRunCommand(fakeCLI)
cmd.SetArgs([]string{"-it", "busybox"})
cmd.SilenceUsage = true
cmdErrC := make(chan error, 1)
go func() {
assert.ErrorIs(t, cmd.ExecuteContext(ctx), context.Canceled)
cmdErrC <- cmd.Execute()
}()
// run command should attempt to attach to the container
select {
case <-time.After(5 * time.Second):
t.Fatal("containerAttachFunc was not called before the 5 second timeout")
t.Fatal("containerAttachFunc was not called before the timeout")
case <-attachCh:
}
assert.NilError(t, syscall.Kill(syscall.Getpid(), syscall.SIGTERM))
assert.NilError(t, syscall.Kill(syscall.Getpid(), syscall.SIGINT))
// end stream from "container" so that we'll detach
conn.Close()
select {
case <-time.After(5 * time.Second):
cancel()
t.Fatal("containerKillFunc was not called before the 5 second timeout")
case <-killCh:
case <-time.After(5 * time.Second):
t.Fatal("containerKillFunc was not called before the timeout")
}
select {
case cmdErr := <-cmdErrC:
assert.Equal(t, cmdErr, cli.StatusError{
StatusCode: 130,
})
case <-time.After(2 * time.Second):
t.Fatal("cmd did not return before the timeout")
}
}

View File

@ -5,6 +5,7 @@ package formatter
import (
"fmt"
"net"
"sort"
"strconv"
"strings"
@ -331,7 +332,8 @@ func DisplayablePorts(ports []types.Port) string {
portKey := port.Type
if port.IP != "" {
if port.PublicPort != current {
hostMappings = append(hostMappings, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type))
hAddrPort := net.JoinHostPort(port.IP, strconv.Itoa(int(port.PublicPort)))
hostMappings = append(hostMappings, fmt.Sprintf("%s->%d/%s", hAddrPort, port.PrivatePort, port.Type))
continue
}
portKey = port.IP + "/" + port.Type

View File

@ -471,6 +471,16 @@ func TestDisplayablePorts(t *testing.T) {
},
"0.0.0.0:0->9988/tcp",
},
{
[]types.Port{
{
IP: "::",
PrivatePort: 9988,
Type: "tcp",
},
},
"[::]:0->9988/tcp",
},
{
[]types.Port{
{

View File

@ -2,6 +2,7 @@ package image
import (
"context"
"errors"
"fmt"
"io"
@ -24,6 +25,7 @@ type imagesOptions struct {
format string
filter opts.FilterOpt
calledAs string
tree bool
}
// NewImagesCommand creates a new `docker images` command
@ -59,6 +61,10 @@ func NewImagesCommand(dockerCLI command.Cli) *cobra.Command {
flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
flags.BoolVar(&options.tree, "tree", false, "List multi-platform images as a tree (EXPERIMENTAL)")
flags.SetAnnotation("tree", "version", []string{"1.47"})
flags.SetAnnotation("tree", "experimentalCLI", nil)
return cmd
}
@ -75,6 +81,26 @@ func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions
filters.Add("reference", options.matchName)
}
if options.tree {
if options.quiet {
return errors.New("--quiet is not yet supported with --tree")
}
if options.noTrunc {
return errors.New("--no-trunc is not yet supported with --tree")
}
if options.showDigests {
return errors.New("--show-digest is not yet supported with --tree")
}
if options.format != "" {
return errors.New("--format is not yet supported with --tree")
}
return runTree(ctx, dockerCLI, treeOptions{
all: options.all,
filters: filters,
})
}
images, err := dockerCLI.Client().ImageList(ctx, image.ListOptions{
All: options.all,
Filters: filters,

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

@ -0,0 +1,393 @@
package image
import (
"context"
"fmt"
"sort"
"strings"
"unicode/utf8"
"github.com/containerd/platforms"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/streams"
"github.com/docker/docker/api/types/filters"
imagetypes "github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"github.com/morikuni/aec"
)
type treeOptions struct {
all bool
filters filters.Args
}
type treeView struct {
images []topImage
// imageSpacing indicates whether there should be extra spacing between images.
imageSpacing bool
}
func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error {
images, err := dockerCLI.Client().ImageList(ctx, imagetypes.ListOptions{
All: opts.all,
Filters: opts.filters,
Manifests: true,
})
if err != nil {
return err
}
view := treeView{
images: make([]topImage, 0, len(images)),
}
for _, img := range images {
details := imageDetails{
ID: img.ID,
DiskUsage: units.HumanSizeWithPrecision(float64(img.Size), 3),
Used: img.Containers > 0,
}
var totalContent int64
children := make([]subImage, 0, len(img.Manifests))
for _, im := range img.Manifests {
if im.Kind != imagetypes.ManifestKindImage {
continue
}
im := im
sub := subImage{
Platform: platforms.Format(im.ImageData.Platform),
Available: im.Available,
Details: imageDetails{
ID: im.ID,
DiskUsage: units.HumanSizeWithPrecision(float64(im.Size.Total), 3),
Used: len(im.ImageData.Containers) > 0,
ContentSize: units.HumanSizeWithPrecision(float64(im.Size.Content), 3),
},
}
if sub.Details.Used {
// Mark top-level parent image as used if any of its subimages are used.
details.Used = true
}
totalContent += im.Size.Content
children = append(children, sub)
// Add extra spacing between images if there's at least one entry with children.
view.imageSpacing = true
}
details.ContentSize = units.HumanSizeWithPrecision(float64(totalContent), 3)
view.images = append(view.images, topImage{
Names: img.RepoTags,
Details: details,
Children: children,
created: img.Created,
})
}
sort.Slice(view.images, func(i, j int) bool {
return view.images[i].created > view.images[j].created
})
return printImageTree(dockerCLI, view)
}
type imageDetails struct {
ID string
DiskUsage string
Used bool
ContentSize string
}
type topImage struct {
Names []string
Details imageDetails
Children []subImage
created int64
}
type subImage struct {
Platform string
Available bool
Details imageDetails
}
const columnSpacing = 3
func printImageTree(dockerCLI command.Cli, view treeView) error {
out := dockerCLI.Out()
_, width := out.GetTtySize()
if width == 0 {
width = 80
}
if width < 20 {
width = 20
}
warningColor := aec.LightYellowF
headerColor := aec.NewBuilder(aec.DefaultF, aec.Bold).ANSI
topNameColor := aec.NewBuilder(aec.BlueF, aec.Underline, aec.Bold).ANSI
normalColor := aec.NewBuilder(aec.DefaultF).ANSI
greenColor := aec.NewBuilder(aec.GreenF).ANSI
untaggedColor := aec.NewBuilder(aec.Faint).ANSI
if !out.IsTerminal() {
headerColor = noColor{}
topNameColor = noColor{}
normalColor = noColor{}
greenColor = noColor{}
warningColor = noColor{}
untaggedColor = noColor{}
}
_, _ = fmt.Fprintln(out, warningColor.Apply("WARNING: This is an experimental feature. The output may change and shouldn't be depended on."))
_, _ = fmt.Fprintln(out, "")
columns := []imgColumn{
{
Title: "Image",
Align: alignLeft,
Width: 0,
},
{
Title: "ID",
Align: alignLeft,
Width: 12,
DetailsValue: func(d *imageDetails) string {
return stringid.TruncateID(d.ID)
},
},
{
Title: "Disk usage",
Align: alignRight,
Width: 10,
DetailsValue: func(d *imageDetails) string {
return d.DiskUsage
},
},
{
Title: "Content size",
Align: alignRight,
Width: 12,
DetailsValue: func(d *imageDetails) string {
return d.ContentSize
},
},
{
Title: "Used",
Align: alignCenter,
Width: 4,
Color: &greenColor,
DetailsValue: func(d *imageDetails) string {
if d.Used {
return "✔"
}
return " "
},
},
}
nameWidth := int(width)
for idx, h := range columns {
if h.Width == 0 {
continue
}
d := h.Width
if idx > 0 {
d += columnSpacing
}
// If the first column gets too short, remove remaining columns
if nameWidth-d < 12 {
columns = columns[:idx]
break
}
nameWidth -= d
}
images := view.images
// Try to make the first column as narrow as possible
widest := widestFirstColumnValue(columns, images)
if nameWidth > widest {
nameWidth = widest
}
columns[0].Width = nameWidth
// Print columns
for i, h := range columns {
if i > 0 {
_, _ = fmt.Fprint(out, strings.Repeat(" ", columnSpacing))
}
_, _ = fmt.Fprint(out, h.Print(headerColor, strings.ToUpper(h.Title)))
}
_, _ = fmt.Fprintln(out)
// Print images
for _, img := range images {
printNames(out, columns, img, topNameColor, untaggedColor)
printDetails(out, columns, normalColor, img.Details)
if len(img.Children) > 0 || view.imageSpacing {
_, _ = fmt.Fprintln(out)
}
printChildren(out, columns, img, normalColor)
_, _ = fmt.Fprintln(out)
}
return nil
}
func printDetails(out *streams.Out, headers []imgColumn, defaultColor aec.ANSI, details imageDetails) {
for _, h := range headers {
if h.DetailsValue == nil {
continue
}
_, _ = fmt.Fprint(out, strings.Repeat(" ", columnSpacing))
clr := defaultColor
if h.Color != nil {
clr = *h.Color
}
val := h.DetailsValue(&details)
_, _ = fmt.Fprint(out, h.Print(clr, val))
}
}
func printChildren(out *streams.Out, headers []imgColumn, img topImage, normalColor aec.ANSI) {
for idx, sub := range img.Children {
clr := normalColor
if !sub.Available {
clr = normalColor.With(aec.Faint)
}
if idx != len(img.Children)-1 {
_, _ = fmt.Fprint(out, headers[0].Print(clr, "├─ "+sub.Platform))
} else {
_, _ = fmt.Fprint(out, headers[0].Print(clr, "└─ "+sub.Platform))
}
printDetails(out, headers, clr, sub.Details)
_, _ = fmt.Fprintln(out, "")
}
}
func printNames(out *streams.Out, headers []imgColumn, img topImage, color, untaggedColor aec.ANSI) {
if len(img.Names) == 0 {
_, _ = fmt.Fprint(out, headers[0].Print(untaggedColor, "<untagged>"))
}
for nameIdx, name := range img.Names {
if nameIdx != 0 {
_, _ = fmt.Fprintln(out, "")
}
_, _ = fmt.Fprint(out, headers[0].Print(color, name))
}
}
type alignment int
const (
alignLeft alignment = iota
alignCenter
alignRight
)
type imgColumn struct {
Title string
Width int
Align alignment
DetailsValue func(*imageDetails) string
Color *aec.ANSI
}
func truncateRunes(s string, length int) string {
runes := []rune(s)
if len(runes) > length {
return string(runes[:length-3]) + "..."
}
return s
}
func (h imgColumn) Print(clr aec.ANSI, s string) string {
switch h.Align {
case alignCenter:
return h.PrintC(clr, s)
case alignRight:
return h.PrintR(clr, s)
case alignLeft:
}
return h.PrintL(clr, s)
}
func (h imgColumn) PrintC(clr aec.ANSI, s string) string {
ln := utf8.RuneCountInString(s)
if ln > h.Width {
return clr.Apply(truncateRunes(s, h.Width))
}
fill := h.Width - ln
l := fill / 2
r := fill - l
return strings.Repeat(" ", l) + clr.Apply(s) + strings.Repeat(" ", r)
}
func (h imgColumn) PrintL(clr aec.ANSI, s string) string {
ln := utf8.RuneCountInString(s)
if ln > h.Width {
return clr.Apply(truncateRunes(s, h.Width))
}
return clr.Apply(s) + strings.Repeat(" ", h.Width-ln)
}
func (h imgColumn) PrintR(clr aec.ANSI, s string) string {
ln := utf8.RuneCountInString(s)
if ln > h.Width {
return clr.Apply(truncateRunes(s, h.Width))
}
return strings.Repeat(" ", h.Width-ln) + clr.Apply(s)
}
type noColor struct{}
func (a noColor) With(_ ...aec.ANSI) aec.ANSI {
return a
}
func (a noColor) Apply(s string) string {
return s
}
func (a noColor) String() string {
return ""
}
// widestFirstColumnValue calculates the width needed to fully display the image names and platforms.
func widestFirstColumnValue(headers []imgColumn, images []topImage) int {
width := len(headers[0].Title)
for _, img := range images {
for _, name := range img.Names {
if len(name) > width {
width = len(name)
}
}
for _, sub := range img.Children {
pl := len(sub.Platform) + len("└─ ")
if pl > width {
width = pl
}
}
}
return width
}

View File

@ -18,7 +18,7 @@ func NewManifestCommand(dockerCli command.Cli) *cobra.Command {
Long: manifestDescription,
Args: cli.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
_, _ = fmt.Fprint(dockerCli.Err(), "\n"+cmd.UsageString())
},
Annotations: map[string]string{"experimentalCLI": ""},
}

View File

@ -41,7 +41,7 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
default:
}
err = ConfigureAuth(ctx, cli, "", "", &authConfig, isDefaultRegistry)
authConfig, err = PromptUserForCredentials(ctx, cli, "", "", authConfig.Username, indexServer)
if err != nil {
return "", err
}
@ -86,8 +86,32 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
return registrytypes.AuthConfig(authconfig), nil
}
// ConfigureAuth handles prompting of user's username and password if needed
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authconfig *registrytypes.AuthConfig, isDefaultRegistry bool) error {
// ConfigureAuth handles prompting of user's username and password if needed.
// Deprecated: use PromptUserForCredentials instead.
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authConfig *registrytypes.AuthConfig, _ bool) error {
defaultUsername := authConfig.Username
serverAddress := authConfig.ServerAddress
newAuthConfig, err := PromptUserForCredentials(ctx, cli, flUser, flPassword, defaultUsername, serverAddress)
if err != nil {
return err
}
authConfig.Username = newAuthConfig.Username
authConfig.Password = newAuthConfig.Password
return nil
}
// PromptUserForCredentials handles the CLI prompt for the user to input
// credentials.
// If argUser is not empty, then the user is only prompted for their password.
// If argPassword is not empty, then the user is only prompted for their username
// If neither argUser nor argPassword are empty, then the user is not prompted and
// an AuthConfig is returned with those values.
// If defaultUsername is not empty, the username prompt includes that username
// and the user can hit enter without inputting a username to use that default
// username.
func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (authConfig registrytypes.AuthConfig, err error) {
// On Windows, force the use of the regular OS stdin stream.
//
// See:
@ -107,13 +131,14 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
// Linux will hit this if you attempt `cat | docker login`, and Windows
// will hit this if you attempt docker login from mintty where stdin
// is a pipe, not a character based console.
if flPassword == "" && !cli.In().IsTerminal() {
return errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
if argPassword == "" && !cli.In().IsTerminal() {
return authConfig, errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
}
authconfig.Username = strings.TrimSpace(authconfig.Username)
isDefaultRegistry := serverAddress == registry.IndexServer
defaultUsername = strings.TrimSpace(defaultUsername)
if flUser = strings.TrimSpace(flUser); flUser == "" {
if argUser = strings.TrimSpace(argUser); argUser == "" {
if isDefaultRegistry {
// if this is a default registry (docker hub), then display the following message.
fmt.Fprintln(cli.Out(), "Log in with your Docker ID or email address to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com/ to create one.")
@ -124,44 +149,43 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
}
var prompt string
if authconfig.Username == "" {
if defaultUsername == "" {
prompt = "Username: "
} else {
prompt = fmt.Sprintf("Username (%s): ", authconfig.Username)
prompt = fmt.Sprintf("Username (%s): ", defaultUsername)
}
var err error
flUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
if err != nil {
return err
return authConfig, err
}
if flUser == "" {
flUser = authconfig.Username
if argUser == "" {
argUser = defaultUsername
}
}
if flUser == "" {
return errors.Errorf("Error: Non-null Username Required")
if argUser == "" {
return authConfig, errors.Errorf("Error: Non-null Username Required")
}
if flPassword == "" {
if argPassword == "" {
restoreInput, err := DisableInputEcho(cli.In())
if err != nil {
return err
return authConfig, err
}
defer restoreInput()
flPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
if err != nil {
return err
return authConfig, err
}
fmt.Fprint(cli.Out(), "\n")
if flPassword == "" {
return errors.Errorf("Error: Password Required")
if argPassword == "" {
return authConfig, errors.Errorf("Error: Password Required")
}
}
authconfig.Username = flUser
authconfig.Password = flPassword
return nil
authConfig.Username = argUser
authConfig.Password = argPassword
authConfig.ServerAddress = serverAddress
return authConfig, nil
}
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete

View File

@ -4,12 +4,15 @@ import (
"context"
"fmt"
"io"
"os"
"strconv"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
configtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/internal/oauth/manager"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
@ -100,80 +103,167 @@ func verifyloginOptions(dockerCli command.Cli, opts *loginOptions) error {
return nil
}
func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error { //nolint:gocyclo
clnt := dockerCli.Client()
func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error {
if err := verifyloginOptions(dockerCli, &opts); err != nil {
return err
}
var (
serverAddress string
response registrytypes.AuthenticateOKBody
response *registrytypes.AuthenticateOKBody
)
if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace {
if opts.serverAddress != "" &&
opts.serverAddress != registry.DefaultNamespace &&
opts.serverAddress != registry.DefaultRegistryHost {
serverAddress = opts.serverAddress
} else {
serverAddress = registry.IndexServer
}
isDefaultRegistry := serverAddress == registry.IndexServer
// attempt login with current (stored) credentials
authConfig, err := command.GetDefaultAuthConfig(dockerCli.ConfigFile(), opts.user == "" && opts.password == "", serverAddress, isDefaultRegistry)
if err == nil && authConfig.Username != "" && authConfig.Password != "" {
response, err = loginWithCredStoreCreds(ctx, dockerCli, &authConfig)
response, err = loginWithStoredCredentials(ctx, dockerCli, authConfig)
}
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
err = command.ConfigureAuth(ctx, dockerCli, opts.user, opts.password, &authConfig, isDefaultRegistry)
if err != nil {
return err
}
response, err = clnt.RegistryLogin(ctx, authConfig)
if err != nil && client.IsErrConnectionFailed(err) {
// If the server isn't responding (yet) attempt to login purely client side
response, err = loginClientSide(ctx, authConfig)
}
// If we (still) have an error, give up
// if we failed to authenticate with stored credentials (or didn't have stored credentials),
// prompt the user for new credentials
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
response, err = loginUser(ctx, dockerCli, opts, authConfig.Username, serverAddress)
if err != nil {
return err
}
}
if response != nil && response.Status != "" {
_, _ = fmt.Fprintln(dockerCli.Out(), response.Status)
}
return nil
}
func loginWithStoredCredentials(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (*registrytypes.AuthenticateOKBody, error) {
_, _ = fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n")
response, err := dockerCli.Client().RegistryLogin(ctx, authConfig)
if err != nil {
if errdefs.IsUnauthorized(err) {
_, _ = fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n")
} else {
_, _ = fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err)
}
}
if response.IdentityToken != "" {
authConfig.Password = ""
authConfig.IdentityToken = response.IdentityToken
}
creds := dockerCli.ConfigFile().GetCredentialsStore(serverAddress)
if err := storeCredentials(dockerCli, authConfig); err != nil {
return nil, err
}
return &response, err
}
const OauthLoginEscapeHatchEnvVar = "DOCKER_CLI_DISABLE_OAUTH_LOGIN"
func isOauthLoginDisabled() bool {
if v := os.Getenv(OauthLoginEscapeHatchEnvVar); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
}
return enabled
}
return false
}
func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) {
// If we're logging into the index server and the user didn't provide a username or password, use the device flow
if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" && !isOauthLoginDisabled() {
response, err := loginWithDeviceCodeFlow(ctx, dockerCli)
// if the error represents a failure to initiate the device-code flow,
// then we fallback to regular cli credentials login
if !errors.Is(err, manager.ErrDeviceLoginStartFail) {
return response, err
}
fmt.Fprint(dockerCli.Err(), "Failed to start web-based login - falling back to command line login...\n\n")
}
return loginWithUsernameAndPassword(ctx, dockerCli, opts, defaultUsername, serverAddress)
}
func loginWithUsernameAndPassword(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) {
// Prompt user for credentials
authConfig, err := command.PromptUserForCredentials(ctx, dockerCli, opts.user, opts.password, defaultUsername, serverAddress)
if err != nil {
return nil, err
}
response, err := loginWithRegistry(ctx, dockerCli, authConfig)
if err != nil {
return nil, err
}
if response.IdentityToken != "" {
authConfig.Password = ""
authConfig.IdentityToken = response.IdentityToken
}
if err = storeCredentials(dockerCli, authConfig); err != nil {
return nil, err
}
return &response, nil
}
func loginWithDeviceCodeFlow(ctx context.Context, dockerCli command.Cli) (*registrytypes.AuthenticateOKBody, error) {
store := dockerCli.ConfigFile().GetCredentialsStore(registry.IndexServer)
authConfig, err := manager.NewManager(store).LoginDevice(ctx, dockerCli.Err())
if err != nil {
return nil, err
}
response, err := loginWithRegistry(ctx, dockerCli, registrytypes.AuthConfig(*authConfig))
if err != nil {
return nil, err
}
if err = storeCredentials(dockerCli, registrytypes.AuthConfig(*authConfig)); err != nil {
return nil, err
}
return &response, nil
}
func storeCredentials(dockerCli command.Cli, authConfig registrytypes.AuthConfig) error {
creds := dockerCli.ConfigFile().GetCredentialsStore(authConfig.ServerAddress)
store, isDefault := creds.(isFileStore)
// Display a warning if we're storing the users password (not a token)
if isDefault && authConfig.Password != "" {
err = displayUnencryptedWarning(dockerCli, store.GetFilename())
err := displayUnencryptedWarning(dockerCli, store.GetFilename())
if err != nil {
return err
}
}
if err := creds.Store(configtypes.AuthConfig(authConfig)); err != nil {
return errors.Errorf("Error saving credentials: %v", err)
}
if response.Status != "" {
fmt.Fprintln(dockerCli.Out(), response.Status)
}
return nil
}
func loginWithCredStoreCreds(ctx context.Context, dockerCli command.Cli, authConfig *registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n")
cliClient := dockerCli.Client()
response, err := cliClient.RegistryLogin(ctx, *authConfig)
if err != nil {
if errdefs.IsUnauthorized(err) {
fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n")
} else {
fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err)
}
func loginWithRegistry(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
response, err := dockerCli.Client().RegistryLogin(ctx, authConfig)
if err != nil && client.IsErrConnectionFailed(err) {
// If the server isn't responding (yet) attempt to login purely client side
response, err = loginClientSide(ctx, authConfig)
}
return response, err
// If we (still) have an error, give up
if err != nil {
return registrytypes.AuthenticateOKBody{}, err
}
return response, nil
}
func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {

View File

@ -74,7 +74,7 @@ func TestLoginWithCredStoreCreds(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
errBuf := new(bytes.Buffer)
cli.SetErr(streams.NewOut(errBuf))
loginWithCredStoreCreds(ctx, cli, &tc.inputAuthConfig)
loginWithStoredCredentials(ctx, cli, tc.inputAuthConfig)
outputString := cli.OutBuffer().String()
assert.Check(t, is.Equal(tc.expectedMsg, outputString))
errorString := errBuf.String()
@ -213,7 +213,9 @@ func TestLoginTermination(t *testing.T) {
runErr := make(chan error)
go func() {
runErr <- runLogin(ctx, cli, loginOptions{})
runErr <- runLogin(ctx, cli, loginOptions{
user: "test-user",
})
}()
// Let the prompt get canceled by the context
@ -226,3 +228,47 @@ func TestLoginTermination(t *testing.T) {
assert.ErrorIs(t, err, command.ErrPromptTerminated)
}
}
func TestIsOauthLoginDisabled(t *testing.T) {
testCases := []struct {
envVar string
disabled bool
}{
{
envVar: "",
disabled: false,
},
{
envVar: "bork",
disabled: false,
},
{
envVar: "0",
disabled: false,
},
{
envVar: "false",
disabled: false,
},
{
envVar: "true",
disabled: true,
},
{
envVar: "TRUE",
disabled: true,
},
{
envVar: "1",
disabled: true,
},
}
for _, tc := range testCases {
t.Setenv(OauthLoginEscapeHatchEnvVar, tc.envVar)
disabled := isOauthLoginDisabled()
assert.Equal(t, disabled, tc.disabled)
}
}

View File

@ -7,6 +7,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/internal/oauth/manager"
"github.com/docker/docker/registry"
"github.com/spf13/cobra"
)
@ -34,7 +35,7 @@ func NewLogoutCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
func runLogout(_ context.Context, dockerCli command.Cli, serverAddress string) error {
func runLogout(ctx context.Context, dockerCli command.Cli, serverAddress string) error {
var isDefaultRegistry bool
if serverAddress == "" {
@ -53,6 +54,13 @@ func runLogout(_ context.Context, dockerCli command.Cli, serverAddress string) e
regsToLogout = append(regsToLogout, hostnameAddress, "http://"+hostnameAddress, "https://"+hostnameAddress)
}
if isDefaultRegistry {
store := dockerCli.ConfigFile().GetCredentialsStore(registry.IndexServer)
if err := manager.NewManager(store).Logout(ctx); err != nil {
fmt.Fprintf(dockerCli.Err(), "WARNING: %v\n", err)
}
}
fmt.Fprintf(dockerCli.Out(), "Removing login credentials for %s\n", hostnameAddress)
errs := make(map[string]error)
for _, r := range regsToLogout {

View File

@ -39,10 +39,10 @@ func runRemove(ctx context.Context, dockerCli command.Cli, sids []string) error
errs = append(errs, err.Error())
continue
}
fmt.Fprintf(dockerCli.Out(), "%s\n", sid)
_, _ = fmt.Fprintf(dockerCli.Out(), "%s\n", sid)
}
if len(errs) > 0 {
return errors.Errorf(strings.Join(errs, "\n"))
return errors.New(strings.Join(errs, "\n"))
}
return nil
}

View File

@ -90,7 +90,7 @@ func runScale(ctx context.Context, dockerCli command.Cli, options *scaleOptions,
if len(errs) == 0 {
return nil
}
return errors.Errorf(strings.Join(errs, "\n"))
return errors.New(strings.Join(errs, "\n"))
}
func runServiceScale(ctx context.Context, dockerCli command.Cli, serviceID string, scale uint64) error {

View File

@ -48,7 +48,7 @@ func RunRemove(ctx context.Context, dockerCli command.Cli, opts options.Remove)
}
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", namespace)
_, _ = fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", namespace)
continue
}
@ -71,7 +71,7 @@ func RunRemove(ctx context.Context, dockerCli command.Cli, opts options.Remove)
}
if len(errs) > 0 {
return errors.Errorf(strings.Join(errs, "\n"))
return errors.New(strings.Join(errs, "\n"))
}
return nil
}

View File

@ -372,7 +372,7 @@ func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) []error {
fprintln(output, " Product License:", info.ProductLicense)
}
if info.DefaultAddressPools != nil && len(info.DefaultAddressPools) > 0 {
if len(info.DefaultAddressPools) > 0 {
fprintln(output, " Default Address Pools:")
for _, pool := range info.DefaultAddressPools {
fprintf(output, " Base: %s, Size: %d\n", pool.Base, pool.Size)

View File

@ -222,7 +222,7 @@ func ValidateOutputPath(path string) error {
}
if err := ValidateOutputPathFileMode(fileInfo.Mode()); err != nil {
return errors.Wrapf(err, fmt.Sprintf("invalid output path: %q must be a directory or a regular file", path))
return errors.Wrapf(err, "invalid output path: %q must be a directory or a regular file", path)
}
}
return nil

View File

@ -48,7 +48,7 @@ func TestCloseRunningCommand(t *testing.T) {
defer close(done)
go func() {
c, err := New(ctx, "sh", "-c", "while true; sleep 1; done")
c, err := New(ctx, "sh", "-c", "while true; do sleep 1; done")
assert.NilError(t, err)
cmdConn := c.(*commandConn)
assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid))
@ -145,7 +145,7 @@ func (mockStdoutEOF) Close() error {
func TestCloseWhileWriting(t *testing.T) {
ctx := context.TODO()
c, err := New(ctx, "sh", "-c", "while true; sleep 1; done")
c, err := New(ctx, "sh", "-c", "while true; do sleep 1; done")
assert.NilError(t, err)
cmdConn := c.(*commandConn)
assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid))
@ -173,7 +173,7 @@ func TestCloseWhileWriting(t *testing.T) {
func TestCloseWhileReading(t *testing.T) {
ctx := context.TODO()
c, err := New(ctx, "sh", "-c", "while true; sleep 1; done")
c, err := New(ctx, "sh", "-c", "while true; do sleep 1; done")
assert.NilError(t, err)
cmdConn := c.(*commandConn)
assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid))

View File

@ -52,6 +52,7 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*ConnectionHelper
args = append(args, "--host", "unix://"+sp.Path)
}
sshFlags = addSSHTimeout(sshFlags)
sshFlags = disablePseudoTerminalAllocation(sshFlags)
args = append(args, "system", "dial-stdio")
return commandconn.New(ctx, "ssh", append(sshFlags, sp.Args(args...)...)...)
},
@ -79,3 +80,14 @@ func addSSHTimeout(sshFlags []string) []string {
}
return sshFlags
}
// disablePseudoTerminalAllocation disables pseudo-terminal allocation to
// prevent SSH from executing as a login shell
func disablePseudoTerminalAllocation(sshFlags []string) []string {
for _, flag := range sshFlags {
if flag == "-T" {
return sshFlags
}
}
return append(sshFlags, "-T")
}

View File

@ -1,6 +1,7 @@
package connhelper
import (
"reflect"
"testing"
"gotest.tools/v3/assert"
@ -29,3 +30,36 @@ func TestSSHFlags(t *testing.T) {
assert.DeepEqual(t, addSSHTimeout(tc.in), tc.out)
}
}
func TestDisablePseudoTerminalAllocation(t *testing.T) {
testCases := []struct {
name string
sshFlags []string
expected []string
}{
{
name: "No -T flag present",
sshFlags: []string{"-v", "-oStrictHostKeyChecking=no"},
expected: []string{"-v", "-oStrictHostKeyChecking=no", "-T"},
},
{
name: "Already contains -T flag",
sshFlags: []string{"-v", "-T", "-oStrictHostKeyChecking=no"},
expected: []string{"-v", "-T", "-oStrictHostKeyChecking=no"},
},
{
name: "Empty sshFlags",
sshFlags: []string{},
expected: []string{"-T"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := disablePseudoTerminalAllocation(tc.sshFlags)
if !reflect.DeepEqual(result, tc.expected) {
t.Errorf("expected %v, got %v", tc.expected, result)
}
})
}
}

View File

@ -0,0 +1,228 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.21
package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"runtime"
"strings"
"time"
"github.com/docker/cli/cli/version"
)
type OAuthAPI interface {
GetDeviceCode(ctx context.Context, audience string) (State, error)
WaitForDeviceToken(ctx context.Context, state State) (TokenResponse, error)
RevokeToken(ctx context.Context, refreshToken string) error
GetAutoPAT(ctx context.Context, audience string, res TokenResponse) (string, error)
}
// API represents API interactions with Auth0.
type API struct {
// TenantURL is the base used for each request to Auth0.
TenantURL string
// ClientID is the client ID for the application to auth with the tenant.
ClientID string
// Scopes are the scopes that are requested during the device auth flow.
Scopes []string
}
// TokenResponse represents the response of the /oauth/token route.
type TokenResponse struct {
AccessToken string `json:"access_token"`
IDToken string `json:"id_token"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
Error *string `json:"error,omitempty"`
ErrorDescription string `json:"error_description,omitempty"`
}
var ErrTimeout = errors.New("timed out waiting for device token")
// GetDeviceCode initiates the device-code auth flow with the tenant.
// The state returned contains the device code that the user must use to
// authenticate, as well as the URL to visit, etc.
func (a API) GetDeviceCode(ctx context.Context, audience string) (State, error) {
data := url.Values{
"client_id": {a.ClientID},
"audience": {audience},
"scope": {strings.Join(a.Scopes, " ")},
}
deviceCodeURL := a.TenantURL + "/oauth/device/code"
resp, err := postForm(ctx, deviceCodeURL, strings.NewReader(data.Encode()))
if err != nil {
return State{}, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return State{}, tryDecodeOAuthError(resp)
}
var state State
err = json.NewDecoder(resp.Body).Decode(&state)
if err != nil {
return state, fmt.Errorf("failed to get device code: %w", err)
}
return state, nil
}
func tryDecodeOAuthError(resp *http.Response) error {
var body map[string]any
if err := json.NewDecoder(resp.Body).Decode(&body); err == nil {
if errorDescription, ok := body["error_description"].(string); ok {
return errors.New(errorDescription)
}
}
return errors.New("unexpected response from tenant: " + resp.Status)
}
// WaitForDeviceToken polls the tenant to get access/refresh tokens for the user.
// This should be called after GetDeviceCode, and will block until the user has
// authenticated or we have reached the time limit for authenticating (based on
// the response from GetDeviceCode).
func (a API) WaitForDeviceToken(ctx context.Context, state State) (TokenResponse, error) {
ticker := time.NewTicker(state.IntervalDuration())
defer ticker.Stop()
timeout := time.After(state.ExpiryDuration())
for {
select {
case <-ctx.Done():
return TokenResponse{}, ctx.Err()
case <-ticker.C:
res, err := a.getDeviceToken(ctx, state)
if err != nil {
return res, err
}
if res.Error != nil {
if *res.Error == "authorization_pending" {
continue
}
return res, errors.New(res.ErrorDescription)
}
return res, nil
case <-timeout:
return TokenResponse{}, ErrTimeout
}
}
}
// getToken calls the token endpoint of Auth0 and returns the response.
func (a API) getDeviceToken(ctx context.Context, state State) (TokenResponse, error) {
data := url.Values{
"client_id": {a.ClientID},
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
"device_code": {state.DeviceCode},
}
oauthTokenURL := a.TenantURL + "/oauth/token"
resp, err := postForm(ctx, oauthTokenURL, strings.NewReader(data.Encode()))
if err != nil {
return TokenResponse{}, fmt.Errorf("failed to get tokens: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()
// this endpoint returns a 403 with an `authorization_pending` error until the
// user has authenticated, so we don't check the status code here and instead
// decode the response and check for the error.
var res TokenResponse
err = json.NewDecoder(resp.Body).Decode(&res)
if err != nil {
return res, fmt.Errorf("failed to decode response: %w", err)
}
return res, nil
}
// RevokeToken revokes a refresh token with the tenant so that it can no longer
// be used to get new tokens.
func (a API) RevokeToken(ctx context.Context, refreshToken string) error {
data := url.Values{
"client_id": {a.ClientID},
"token": {refreshToken},
}
revokeURL := a.TenantURL + "/oauth/revoke"
resp, err := postForm(ctx, revokeURL, strings.NewReader(data.Encode()))
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return tryDecodeOAuthError(resp)
}
return nil
}
func postForm(ctx context.Context, reqURL string, data io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, data)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
cliVersion := strings.ReplaceAll(version.Version, ".", "_")
req.Header.Set("User-Agent", fmt.Sprintf("docker-cli:%s:%s-%s", cliVersion, runtime.GOOS, runtime.GOARCH))
return http.DefaultClient.Do(req)
}
func (a API) GetAutoPAT(ctx context.Context, audience string, res TokenResponse) (string, error) {
patURL := audience + "/v2/access-tokens/desktop-generate"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, patURL, nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+res.AccessToken)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("unexpected response from Hub: %s", resp.Status)
}
var response patGenerateResponse
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return "", err
}
return response.Data.Token, nil
}
type patGenerateResponse struct {
Data struct {
Token string `json:"token"`
}
}

View File

@ -0,0 +1,428 @@
package api
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"gotest.tools/v3/assert"
)
func TestGetDeviceCode(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) {
t.Parallel()
var clientID, audience, scope, path string
expectedState := State{
DeviceCode: "aDeviceCode",
UserCode: "aUserCode",
VerificationURI: "aVerificationURI",
ExpiresIn: 60,
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
clientID = r.FormValue("client_id")
audience = r.FormValue("audience")
scope = r.FormValue("scope")
path = r.URL.Path
jsonState, err := json.Marshal(expectedState)
assert.NilError(t, err)
_, _ = w.Write(jsonState)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
state, err := api.GetDeviceCode(context.Background(), "anAudience")
assert.NilError(t, err)
assert.DeepEqual(t, expectedState, state)
assert.Equal(t, clientID, "aClientID")
assert.Equal(t, audience, "anAudience")
assert.Equal(t, scope, "bork meow")
assert.Equal(t, path, "/oauth/device/code")
})
t.Run("error w/ description", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
jsonState, err := json.Marshal(TokenResponse{
ErrorDescription: "invalid audience",
})
assert.NilError(t, err)
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write(jsonState)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
_, err := api.GetDeviceCode(context.Background(), "bad_audience")
assert.ErrorContains(t, err, "invalid audience")
})
t.Run("general error", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "an error", http.StatusInternalServerError)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
_, err := api.GetDeviceCode(context.Background(), "anAudience")
assert.ErrorContains(t, err, "unexpected response from tenant: 500 Internal Server Error")
})
t.Run("canceled context", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
time.Sleep(2 * time.Second)
http.Error(w, "an error", http.StatusInternalServerError)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(1 * time.Second)
cancel()
}()
_, err := api.GetDeviceCode(ctx, "anAudience")
assert.ErrorContains(t, err, "context canceled")
})
}
func TestWaitForDeviceToken(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) {
t.Parallel()
expectedToken := TokenResponse{
AccessToken: "a-real-token",
IDToken: "",
RefreshToken: "the-refresh-token",
Scope: "",
ExpiresIn: 3600,
TokenType: "",
}
var respond atomic.Bool
go func() {
time.Sleep(5 * time.Second)
respond.Store(true)
}()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/oauth/token", r.URL.Path)
assert.Equal(t, r.FormValue("client_id"), "aClientID")
assert.Equal(t, r.FormValue("grant_type"), "urn:ietf:params:oauth:grant-type:device_code")
assert.Equal(t, r.FormValue("device_code"), "aDeviceCode")
if respond.Load() {
jsonState, err := json.Marshal(expectedToken)
assert.NilError(t, err)
w.Write(jsonState)
} else {
pendingError := "authorization_pending"
jsonResponse, err := json.Marshal(TokenResponse{
Error: &pendingError,
})
assert.NilError(t, err)
w.Write(jsonResponse)
}
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
state := State{
DeviceCode: "aDeviceCode",
UserCode: "aUserCode",
Interval: 1,
ExpiresIn: 30,
}
token, err := api.WaitForDeviceToken(context.Background(), state)
assert.NilError(t, err)
assert.DeepEqual(t, token, expectedToken)
})
t.Run("timeout", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/oauth/token", r.URL.Path)
assert.Equal(t, r.FormValue("client_id"), "aClientID")
assert.Equal(t, r.FormValue("grant_type"), "urn:ietf:params:oauth:grant-type:device_code")
assert.Equal(t, r.FormValue("device_code"), "aDeviceCode")
pendingError := "authorization_pending"
jsonResponse, err := json.Marshal(TokenResponse{
Error: &pendingError,
})
assert.NilError(t, err)
w.Write(jsonResponse)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
state := State{
DeviceCode: "aDeviceCode",
UserCode: "aUserCode",
Interval: 1,
ExpiresIn: 1,
}
_, err := api.WaitForDeviceToken(context.Background(), state)
assert.ErrorIs(t, err, ErrTimeout)
})
t.Run("canceled context", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
pendingError := "authorization_pending"
jsonResponse, err := json.Marshal(TokenResponse{
Error: &pendingError,
})
assert.NilError(t, err)
w.Write(jsonResponse)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
state := State{
DeviceCode: "aDeviceCode",
UserCode: "aUserCode",
Interval: 1,
ExpiresIn: 5,
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(1 * time.Second)
cancel()
}()
_, err := api.WaitForDeviceToken(ctx, state)
assert.ErrorContains(t, err, "context canceled")
})
}
func TestRevoke(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/oauth/revoke", r.URL.Path)
assert.Equal(t, r.FormValue("client_id"), "aClientID")
assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token")
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
err := api.RevokeToken(context.Background(), "v1.a-refresh-token")
assert.NilError(t, err)
})
t.Run("unexpected response", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/oauth/revoke", r.URL.Path)
assert.Equal(t, r.FormValue("client_id"), "aClientID")
assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token")
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
err := api.RevokeToken(context.Background(), "v1.a-refresh-token")
assert.ErrorContains(t, err, "unexpected response from tenant: 404 Not Found")
})
t.Run("error w/ description", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jsonState, err := json.Marshal(TokenResponse{
ErrorDescription: "invalid client id",
})
assert.NilError(t, err)
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write(jsonState)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
err := api.RevokeToken(context.Background(), "v1.a-refresh-token")
assert.ErrorContains(t, err, "invalid client id")
})
t.Run("canceled context", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/oauth/revoke", r.URL.Path)
assert.Equal(t, r.FormValue("client_id"), "aClientID")
assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token")
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
err := api.RevokeToken(ctx, "v1.a-refresh-token")
assert.ErrorContains(t, err, "context canceled")
})
}
func TestGetAutoPAT(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/v2/access-tokens/desktop-generate", r.URL.Path)
assert.Equal(t, "Bearer bork", r.Header.Get("Authorization"))
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
marshalledResponse, err := json.Marshal(patGenerateResponse{
Data: struct {
Token string `json:"token"`
}{
Token: "a-docker-pat",
},
})
assert.NilError(t, err)
w.WriteHeader(http.StatusCreated)
w.Write(marshalledResponse)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
pat, err := api.GetAutoPAT(context.Background(), ts.URL, TokenResponse{
AccessToken: "bork",
})
assert.NilError(t, err)
assert.Equal(t, "a-docker-pat", pat)
})
t.Run("general error", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
_, err := api.GetAutoPAT(context.Background(), ts.URL, TokenResponse{
AccessToken: "bork",
})
assert.ErrorContains(t, err, "unexpected response from Hub: 500 Internal Server Error")
})
t.Run("context canceled", func(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/v2/access-tokens/desktop-generate", r.URL.Path)
assert.Equal(t, "Bearer bork", r.Header.Get("Authorization"))
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
marshalledResponse, err := json.Marshal(patGenerateResponse{
Data: struct {
Token string `json:"token"`
}{
Token: "a-docker-pat",
},
})
assert.NilError(t, err)
time.Sleep(500 * time.Millisecond)
w.WriteHeader(http.StatusCreated)
w.Write(marshalledResponse)
}))
defer ts.Close()
api := API{
TenantURL: ts.URL,
ClientID: "aClientID",
Scopes: []string{"bork", "meow"},
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
pat, err := api.GetAutoPAT(ctx, ts.URL, TokenResponse{
AccessToken: "bork",
})
assert.ErrorContains(t, err, "context canceled")
assert.Equal(t, "", pat)
})
}

View File

@ -0,0 +1,26 @@
package api
import (
"time"
)
// State represents the state of exchange after submitting.
type State struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri_complete"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
// IntervalDuration returns the duration that should be waited between each auth
// polling event.
func (s State) IntervalDuration() time.Duration {
return time.Second * time.Duration(s.Interval)
}
// ExpiryDuration returns the total duration for which the client should keep
// polling.
func (s State) ExpiryDuration() time.Duration {
return time.Second * time.Duration(s.ExpiresIn)
}

73
cli/internal/oauth/jwt.go Normal file
View File

@ -0,0 +1,73 @@
package oauth
import (
"github.com/go-jose/go-jose/v3/jwt"
)
// Claims represents standard claims along with some custom ones.
type Claims struct {
jwt.Claims
// Domain is the domain claims for the token.
Domain DomainClaims `json:"https://hub.docker.com"`
// Scope is the scopes for the claims as a string that is space delimited.
Scope string `json:"scope,omitempty"`
}
// DomainClaims represents a custom claim data set that doesn't change the spec
// payload. This is primarily introduced by Auth0 and is defined by a fully
// specified URL as it's key. e.g. "https://hub.docker.com"
type DomainClaims struct {
// UUID is the user, machine client, or organization's UUID in our database.
UUID string `json:"uuid"`
// Email is the user's email address.
Email string `json:"email"`
// Username is the user's username.
Username string `json:"username"`
// Source is the source of the JWT. This should look like
// `docker_{type}|{id}`.
Source string `json:"source"`
// SessionID is the unique ID of the token.
SessionID string `json:"session_id"`
// ClientID is the client_id that generated the token. This is filled if
// M2M.
ClientID string `json:"client_id,omitempty"`
// ClientName is the name of the client that generated the token. This is
// filled if M2M.
ClientName string `json:"client_name,omitempty"`
}
// Source represents a source of a JWT.
type Source struct {
// Type is the type of source. This could be "pat" etc.
Type string `json:"type"`
// ID is the identifier to the source type. If "pat" then this will be the
// ID of the PAT.
ID string `json:"id"`
}
// GetClaims returns claims from an access token without verification.
func GetClaims(accessToken string) (claims Claims, err error) {
token, err := parseSigned(accessToken)
if err != nil {
return
}
err = token.UnsafeClaimsWithoutVerification(&claims)
return
}
// parseSigned parses a JWT and returns the signature object or error. This does
// not verify the validity of the JWT.
func parseSigned(token string) (*jwt.JSONWebToken, error) {
return jwt.ParseSigned(token)
}

View File

@ -0,0 +1,204 @@
package manager
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/internal/oauth"
"github.com/docker/cli/cli/internal/oauth/api"
"github.com/docker/docker/registry"
"github.com/morikuni/aec"
"github.com/sirupsen/logrus"
"github.com/pkg/browser"
)
// OAuthManager is the manager responsible for handling authentication
// flows with the oauth tenant.
type OAuthManager struct {
store credentials.Store
tenant string
audience string
clientID string
api api.OAuthAPI
openBrowser func(string) error
}
// OAuthManagerOptions are the options used for New to create a new auth manager.
type OAuthManagerOptions struct {
Store credentials.Store
Audience string
ClientID string
Scopes []string
Tenant string
DeviceName string
OpenBrowser func(string) error
}
func New(options OAuthManagerOptions) *OAuthManager {
scopes := []string{"openid", "offline_access"}
if len(options.Scopes) > 0 {
scopes = options.Scopes
}
openBrowser := options.OpenBrowser
if openBrowser == nil {
// Prevent errors from missing binaries (like xdg-open) from
// cluttering the output. We can handle errors ourselves.
browser.Stdout = io.Discard
browser.Stderr = io.Discard
openBrowser = browser.OpenURL
}
return &OAuthManager{
clientID: options.ClientID,
audience: options.Audience,
tenant: options.Tenant,
store: options.Store,
api: api.API{
TenantURL: "https://" + options.Tenant,
ClientID: options.ClientID,
Scopes: scopes,
},
openBrowser: openBrowser,
}
}
var ErrDeviceLoginStartFail = errors.New("failed to start device code flow login")
// LoginDevice launches the device authentication flow with the tenant,
// printing instructions to the provided writer and attempting to open the
// browser for the user to authenticate.
// After the user completes the browser login, LoginDevice uses the retrieved
// tokens to create a Hub PAT which is returned to the caller.
// The retrieved tokens are stored in the credentials store (under a separate
// key), and the refresh token is concatenated with the client ID.
func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.AuthConfig, error) {
state, err := m.api.GetDeviceCode(ctx, m.audience)
if err != nil {
logrus.Debugf("failed to start device code login: %v", err)
return nil, ErrDeviceLoginStartFail
}
if state.UserCode == "" {
logrus.Debugf("failed to start device code login: missing user code")
return nil, ErrDeviceLoginStartFail
}
_, _ = fmt.Fprintln(w, aec.Bold.Apply("\nUSING WEB BASED LOGIN"))
_, _ = fmt.Fprintln(w, "To sign in with credentials on the command line, use 'docker login -u <username>'")
_, _ = fmt.Fprintf(w, "\nYour one-time device confirmation code is: "+aec.Bold.Apply("%s\n"), state.UserCode)
_, _ = fmt.Fprintf(w, aec.Bold.Apply("Press ENTER")+" to open your browser or submit your device code here: "+aec.Underline.Apply("%s\n"), strings.Split(state.VerificationURI, "?")[0])
tokenResChan := make(chan api.TokenResponse)
waitForTokenErrChan := make(chan error)
go func() {
tokenRes, err := m.api.WaitForDeviceToken(ctx, state)
if err != nil {
waitForTokenErrChan <- err
return
}
tokenResChan <- tokenRes
}()
go func() {
reader := bufio.NewReader(os.Stdin)
_, _ = reader.ReadString('\n')
_ = m.openBrowser(state.VerificationURI)
}()
_, _ = fmt.Fprint(w, "\nWaiting for authentication in the browser…\n")
var tokenRes api.TokenResponse
select {
case <-ctx.Done():
return nil, errors.New("login canceled")
case err := <-waitForTokenErrChan:
return nil, fmt.Errorf("failed waiting for authentication: %w", err)
case tokenRes = <-tokenResChan:
}
claims, err := oauth.GetClaims(tokenRes.AccessToken)
if err != nil {
return nil, fmt.Errorf("failed to parse token claims: %w", err)
}
err = m.storeTokensInStore(tokenRes, claims.Domain.Username)
if err != nil {
return nil, fmt.Errorf("failed to store tokens: %w", err)
}
pat, err := m.api.GetAutoPAT(ctx, m.audience, tokenRes)
if err != nil {
return nil, err
}
return &types.AuthConfig{
Username: claims.Domain.Username,
Password: pat,
ServerAddress: registry.IndexServer,
}, nil
}
// Logout fetches the refresh token from the store and revokes it
// with the configured oauth tenant. The stored access and refresh
// tokens are then erased from the store.
// If the refresh token is not found in the store, an error is not
// returned.
func (m *OAuthManager) Logout(ctx context.Context) error {
refreshConfig, err := m.store.Get(refreshTokenKey)
if err != nil {
return err
}
if refreshConfig.Password == "" {
return nil
}
parts := strings.Split(refreshConfig.Password, "..")
if len(parts) != 2 {
// the token wasn't stored by the CLI, so don't revoke it
// or erase it from the store/error
return nil
}
// erase the token from the store first, that way
// if the revoke fails, the user can try to logout again
if err := m.eraseTokensFromStore(); err != nil {
return fmt.Errorf("failed to erase tokens: %w", err)
}
if err := m.api.RevokeToken(ctx, parts[0]); err != nil {
return fmt.Errorf("credentials erased successfully, but there was a failure to revoke the OAuth refresh token with the tenant: %w", err)
}
return nil
}
const (
accessTokenKey = registry.IndexServer + "access-token"
refreshTokenKey = registry.IndexServer + "refresh-token"
)
func (m *OAuthManager) storeTokensInStore(tokens api.TokenResponse, username string) error {
return errors.Join(
m.store.Store(types.AuthConfig{
Username: username,
Password: tokens.AccessToken,
ServerAddress: accessTokenKey,
}),
m.store.Store(types.AuthConfig{
Username: username,
Password: tokens.RefreshToken + ".." + m.clientID,
ServerAddress: refreshTokenKey,
}),
)
}
func (m *OAuthManager) eraseTokensFromStore() error {
return errors.Join(
m.store.Erase(accessTokenKey),
m.store.Erase(refreshTokenKey),
)
}

View File

@ -0,0 +1,363 @@
package manager
import (
"context"
"errors"
"os"
"testing"
"time"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/internal/oauth/api"
"gotest.tools/v3/assert"
)
const (
//nolint:lll
validToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InhYa3BCdDNyV3MyRy11YjlscEpncSJ9.eyJodHRwczovL2h1Yi5kb2NrZXIuY29tIjp7ImVtYWlsIjoiYm9ya0Bkb2NrZXIuY29tIiwic2Vzc2lvbl9pZCI6ImEtc2Vzc2lvbi1pZCIsInNvdXJjZSI6InNhbWxwIiwidXNlcm5hbWUiOiJib3JrISIsInV1aWQiOiIwMTIzLTQ1Njc4OSJ9LCJpc3MiOiJodHRwczovL2xvZ2luLmRvY2tlci5jb20vIiwic3ViIjoic2FtbHB8c2FtbHAtZG9ja2VyfGJvcmtAZG9ja2VyLmNvbSIsImF1ZCI6WyJodHRwczovL2F1ZGllbmNlLmNvbSJdLCJpYXQiOjE3MTk1MDI5MzksImV4cCI6MTcxOTUwNjUzOSwic2NvcGUiOiJvcGVuaWQgb2ZmbGluZV9hY2Nlc3MifQ.VUSp-9_SOvMPWJPRrSh7p4kSPoye4DA3kyd2I0TW0QtxYSRq7xCzNj0NC_ywlPlKBFBeXKm4mh93d1vBSh79I9Heq5tj0Fr4KH77U5xJRMEpjHqoT5jxMEU1hYXX92xctnagBMXxDvzUfu3Yf0tvYSA0RRoGbGTHfdYYRwOrGbwQ75Qg1dyIxUkwsG053eYX2XkmLGxymEMgIq_gWksgAamOc40_0OCdGr-MmDeD2HyGUa309aGltzQUw7Z0zG1AKSXy3WwfMHdWNFioTAvQphwEyY3US8ybSJi78upSFTjwUcryMeHUwQ3uV9PxwPMyPoYxo1izVB-OUJxM8RqEbg"
)
// parsed token:
// {
// "https://hub.docker.com": {
// "email": "bork@docker.com",
// "session_id": "a-session-id",
// "source": "samlp",
// "username": "bork!",
// "uuid": "0123-456789"
// },
// "iss": "https://login.docker.com/",
// "sub": "samlp|samlp-docker|bork@docker.com",
// "aud": [
// "https://audience.com"
// ],
// "iat": 1719502939,
// "exp": 1719506539,
// "scope": "openid offline_access"
// }
func TestLoginDevice(t *testing.T) {
t.Run("valid token", func(t *testing.T) {
expectedState := api.State{
DeviceCode: "device-code",
UserCode: "0123-4567",
VerificationURI: "an-url",
ExpiresIn: 300,
}
var receivedAudience string
getDeviceToken := func(audience string) (api.State, error) {
receivedAudience = audience
return expectedState, nil
}
var receivedState api.State
waitForDeviceToken := func(state api.State) (api.TokenResponse, error) {
receivedState = state
return api.TokenResponse{
AccessToken: validToken,
RefreshToken: "refresh-token",
}, nil
}
var receivedAccessToken, getPatReceivedAudience string
getAutoPat := func(audience string, res api.TokenResponse) (string, error) {
receivedAccessToken = res.AccessToken
getPatReceivedAudience = audience
return "a-pat", nil
}
api := &testAPI{
getDeviceToken: getDeviceToken,
waitForDeviceToken: waitForDeviceToken,
getAutoPAT: getAutoPat,
}
store := newStore(map[string]types.AuthConfig{})
manager := OAuthManager{
store: credentials.NewFileStore(store),
audience: "https://hub.docker.com",
api: api,
openBrowser: func(url string) error {
return nil
},
}
authConfig, err := manager.LoginDevice(context.Background(), os.Stderr)
assert.NilError(t, err)
assert.Equal(t, receivedAudience, "https://hub.docker.com")
assert.Equal(t, receivedState, expectedState)
assert.DeepEqual(t, authConfig, &types.AuthConfig{
Username: "bork!",
Password: "a-pat",
ServerAddress: "https://index.docker.io/v1/",
})
assert.Equal(t, receivedAccessToken, validToken)
assert.Equal(t, getPatReceivedAudience, "https://hub.docker.com")
})
t.Run("stores in cred store", func(t *testing.T) {
getDeviceToken := func(audience string) (api.State, error) {
return api.State{
DeviceCode: "device-code",
UserCode: "0123-4567",
}, nil
}
waitForDeviceToken := func(state api.State) (api.TokenResponse, error) {
return api.TokenResponse{
AccessToken: validToken,
RefreshToken: "refresh-token",
}, nil
}
getAutoPAT := func(audience string, res api.TokenResponse) (string, error) {
return "a-pat", nil
}
a := &testAPI{
getDeviceToken: getDeviceToken,
waitForDeviceToken: waitForDeviceToken,
getAutoPAT: getAutoPAT,
}
store := newStore(map[string]types.AuthConfig{})
manager := OAuthManager{
clientID: "client-id",
store: credentials.NewFileStore(store),
api: a,
openBrowser: func(url string) error {
return nil
},
}
authConfig, err := manager.LoginDevice(context.Background(), os.Stderr)
assert.NilError(t, err)
assert.Equal(t, authConfig.Password, "a-pat")
assert.Equal(t, authConfig.Username, "bork!")
assert.Equal(t, len(store.configs), 2)
assert.Equal(t, store.configs["https://index.docker.io/v1/access-token"].Password, validToken)
assert.Equal(t, store.configs["https://index.docker.io/v1/refresh-token"].Password, "refresh-token..client-id")
})
t.Run("timeout", func(t *testing.T) {
getDeviceToken := func(audience string) (api.State, error) {
return api.State{
DeviceCode: "device-code",
UserCode: "0123-4567",
VerificationURI: "an-url",
ExpiresIn: 300,
}, nil
}
waitForDeviceToken := func(state api.State) (api.TokenResponse, error) {
return api.TokenResponse{}, api.ErrTimeout
}
a := &testAPI{
getDeviceToken: getDeviceToken,
waitForDeviceToken: waitForDeviceToken,
}
manager := OAuthManager{
api: a,
openBrowser: func(url string) error {
return nil
},
}
_, err := manager.LoginDevice(context.Background(), os.Stderr)
assert.ErrorContains(t, err, "failed waiting for authentication: timed out waiting for device token")
})
t.Run("canceled context", func(t *testing.T) {
getDeviceToken := func(audience string) (api.State, error) {
return api.State{
DeviceCode: "device-code",
UserCode: "0123-4567",
}, nil
}
waitForDeviceToken := func(state api.State) (api.TokenResponse, error) {
// make sure that the context is cancelled before this returns
time.Sleep(500 * time.Millisecond)
return api.TokenResponse{
AccessToken: validToken,
RefreshToken: "refresh-token",
}, nil
}
a := &testAPI{
getDeviceToken: getDeviceToken,
waitForDeviceToken: waitForDeviceToken,
}
manager := OAuthManager{
api: a,
openBrowser: func(url string) error {
return nil
},
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := manager.LoginDevice(ctx, os.Stderr)
assert.ErrorContains(t, err, "login canceled")
})
}
func TestLogout(t *testing.T) {
t.Run("successfully revokes token", func(t *testing.T) {
var receivedToken string
a := &testAPI{
revokeToken: func(token string) error {
receivedToken = token
return nil
},
}
store := newStore(map[string]types.AuthConfig{
"https://index.docker.io/v1/access-token": {
Password: validToken,
},
"https://index.docker.io/v1/refresh-token": {
Password: "a-refresh-token..client-id",
},
})
manager := OAuthManager{
store: credentials.NewFileStore(store),
api: a,
}
err := manager.Logout(context.Background())
assert.NilError(t, err)
assert.Equal(t, receivedToken, "a-refresh-token")
assert.Equal(t, len(store.configs), 0)
})
t.Run("error revoking token", func(t *testing.T) {
a := &testAPI{
revokeToken: func(token string) error {
return errors.New("couldn't reach tenant")
},
}
store := newStore(map[string]types.AuthConfig{
"https://index.docker.io/v1/access-token": {
Password: validToken,
},
"https://index.docker.io/v1/refresh-token": {
Password: "a-refresh-token..client-id",
},
})
manager := OAuthManager{
store: credentials.NewFileStore(store),
api: a,
}
err := manager.Logout(context.Background())
assert.ErrorContains(t, err, "credentials erased successfully, but there was a failure to revoke the OAuth refresh token with the tenant: couldn't reach tenant")
assert.Equal(t, len(store.configs), 0)
})
t.Run("invalid refresh token", func(t *testing.T) {
var triedRevoke bool
a := &testAPI{
revokeToken: func(token string) error {
triedRevoke = true
return nil
},
}
store := newStore(map[string]types.AuthConfig{
"https://index.docker.io/v1/access-token": {
Password: validToken,
},
"https://index.docker.io/v1/refresh-token": {
Password: "a-refresh-token-without-client-id",
},
})
manager := OAuthManager{
store: credentials.NewFileStore(store),
api: a,
}
err := manager.Logout(context.Background())
assert.NilError(t, err)
assert.Check(t, !triedRevoke)
})
t.Run("no refresh token", func(t *testing.T) {
a := &testAPI{}
var triedRevoke bool
revokeToken := func(token string) error {
triedRevoke = true
return nil
}
a.revokeToken = revokeToken
store := newStore(map[string]types.AuthConfig{})
manager := OAuthManager{
store: credentials.NewFileStore(store),
api: a,
}
err := manager.Logout(context.Background())
assert.NilError(t, err)
assert.Check(t, !triedRevoke)
})
}
var _ api.OAuthAPI = &testAPI{}
type testAPI struct {
getDeviceToken func(audience string) (api.State, error)
waitForDeviceToken func(state api.State) (api.TokenResponse, error)
refresh func(token string) (api.TokenResponse, error)
revokeToken func(token string) error
getAutoPAT func(audience string, res api.TokenResponse) (string, error)
}
func (t *testAPI) GetDeviceCode(_ context.Context, audience string) (api.State, error) {
if t.getDeviceToken != nil {
return t.getDeviceToken(audience)
}
return api.State{}, nil
}
func (t *testAPI) WaitForDeviceToken(_ context.Context, state api.State) (api.TokenResponse, error) {
if t.waitForDeviceToken != nil {
return t.waitForDeviceToken(state)
}
return api.TokenResponse{}, nil
}
func (t *testAPI) Refresh(_ context.Context, token string) (api.TokenResponse, error) {
if t.refresh != nil {
return t.refresh(token)
}
return api.TokenResponse{}, nil
}
func (t *testAPI) RevokeToken(_ context.Context, token string) error {
if t.revokeToken != nil {
return t.revokeToken(token)
}
return nil
}
func (t *testAPI) GetAutoPAT(_ context.Context, audience string, res api.TokenResponse) (string, error) {
if t.getAutoPAT != nil {
return t.getAutoPAT(audience, res)
}
return "", nil
}
type fakeStore struct {
configs map[string]types.AuthConfig
}
func (f *fakeStore) Save() error {
return nil
}
func (f *fakeStore) GetAuthConfigs() map[string]types.AuthConfig {
return f.configs
}
func (f *fakeStore) GetFilename() string {
return "/tmp/docker-fakestore"
}
func newStore(auths map[string]types.AuthConfig) *fakeStore {
return &fakeStore{configs: auths}
}

View File

@ -0,0 +1,28 @@
package manager
import (
"fmt"
"runtime"
"strings"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/version"
)
const (
audience = "https://hub.docker.com"
tenant = "login.docker.com"
clientID = "L4v0dmlNBpYUjGGab0C2JtgTgXr1Qz4d"
)
func NewManager(store credentials.Store) *OAuthManager {
cliVersion := strings.ReplaceAll(version.Version, ".", "_")
options := OAuthManagerOptions{
Store: store,
Audience: audience,
ClientID: clientID,
Tenant: tenant,
DeviceName: fmt.Sprintf("docker-cli:%s:%s-%s", cliVersion, runtime.GOOS, runtime.GOARCH),
}
return New(options)
}

View File

@ -27,16 +27,16 @@ func NoArgs(cmd *cobra.Command, args []string) error {
}
// RequiresMinArgs returns an error if there is not at least min args
func RequiresMinArgs(min int) cobra.PositionalArgs {
func RequiresMinArgs(minArgs int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) >= min {
if len(args) >= minArgs {
return nil
}
return errors.Errorf(
"%q requires at least %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s",
cmd.CommandPath(),
min,
pluralize("argument", min),
minArgs,
pluralize("argument", minArgs),
cmd.CommandPath(),
cmd.UseLine(),
cmd.Short,
@ -45,16 +45,16 @@ func RequiresMinArgs(min int) cobra.PositionalArgs {
}
// RequiresMaxArgs returns an error if there is not at most max args
func RequiresMaxArgs(max int) cobra.PositionalArgs {
func RequiresMaxArgs(maxArgs int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) <= max {
if len(args) <= maxArgs {
return nil
}
return errors.Errorf(
"%q requires at most %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s",
cmd.CommandPath(),
max,
pluralize("argument", max),
maxArgs,
pluralize("argument", maxArgs),
cmd.CommandPath(),
cmd.UseLine(),
cmd.Short,
@ -63,17 +63,17 @@ func RequiresMaxArgs(max int) cobra.PositionalArgs {
}
// RequiresRangeArgs returns an error if there is not at least min args and at most max args
func RequiresRangeArgs(min int, max int) cobra.PositionalArgs {
func RequiresRangeArgs(minArgs int, maxArgs int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) >= min && len(args) <= max {
if len(args) >= minArgs && len(args) <= maxArgs {
return nil
}
return errors.Errorf(
"%q requires at least %d and at most %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s",
cmd.CommandPath(),
min,
max,
pluralize("argument", max),
minArgs,
maxArgs,
pluralize("argument", maxArgs),
cmd.CommandPath(),
cmd.UseLine(),
cmd.Short,

View File

@ -1,5 +1,5 @@
variable "GO_VERSION" {
default = "1.21.12"
default = "1.21.13"
}
variable "VERSION" {
default = ""

View File

@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.21.12
ARG GO_VERSION=1.21.13
ARG ALPINE_VERSION=3.20
ARG BUILDX_VERSION=0.16.1

View File

@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.21.12
ARG GO_VERSION=1.21.13
ARG ALPINE_VERSION=3.20
ARG GOLANGCI_LINT_VERSION=v1.59.1

View File

@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.21.12
ARG GO_VERSION=1.21.13
ARG ALPINE_VERSION=3.20
ARG MODOUTDATED_VERSION=v0.8.0

View File

@ -562,8 +562,7 @@ Docker API v1.42 and up now ignores this option when set. Older versions of the
API continue to accept the option, but depending on the OCI runtime used, may
take no effect.
> **Note**
>
> [!NOTE]
> While not deprecated (yet) in Docker, the OCI runtime specification also
> deprecated the `memory.kmem.tcp.limit_in_bytes` option. When using `runc` as
> runtime, this option takes no effect. The linux kernel did not explicitly

View File

@ -16,8 +16,7 @@ plugins using Docker Engine.
For information about legacy (non-managed) plugins, refer to
[Understand legacy Docker Engine plugins](legacy_plugins.md).
> **Note**
>
> [!NOTE]
> Docker Engine managed plugins are currently not supported on Windows daemons.
## Installing and using a plugin
@ -38,8 +37,7 @@ operation, such as creating a volume.
In the following example, you install the `sshfs` plugin, verify that it is
enabled, and use it to create a volume.
> **Note**
>
> [!NOTE]
> This example is intended for instructional purposes only. Once the volume is
> created, your SSH password to the remote host is exposed as plaintext when
> inspecting the volume. Delete the volume as soon as you are done with the
@ -126,8 +124,7 @@ commands and options, see the
The `rootfs` directory represents the root filesystem of the plugin. In this
example, it was created from a Dockerfile:
> **Note**
>
> [!NOTE]
> The `/run/docker/plugins` directory is mandatory inside of the
> plugin's filesystem for Docker to communicate with the plugin.

View File

@ -43,8 +43,7 @@ Authorization plugins must follow the rules described in [Docker Plugin API](plu
Each plugin must reside within directories described under the
[Plugin discovery](plugin_api.md#plugin-discovery) section.
> **Note**
>
> [!NOTE]
> The abbreviations `AuthZ` and `AuthN` mean authorization and authentication
> respectively.

View File

@ -8,8 +8,7 @@ Docker exposes internal metrics based on the Prometheus format. Metrics plugins
enable accessing these metrics in a consistent way by providing a Unix
socket at a predefined path where the plugin can scrape the metrics.
> **Note**
>
> [!NOTE]
> While the plugin interface for metrics is non-experimental, the naming of the
> metrics and metric labels is still considered experimental and may change in a
> future version.

View File

@ -80,8 +80,7 @@ provide the Docker Daemon with writeable paths on the host filesystem. The Docke
daemon provides these paths to containers to consume. The Docker daemon makes
the volumes available by bind-mounting the provided paths into the containers.
> **Note**
>
> [!NOTE]
> Volume plugins should *not* write data to the `/var/lib/docker/` directory,
> including `/var/lib/docker/volumes`. The `/var/lib/docker/` directory is
> reserved for Docker.

View File

@ -19,8 +19,7 @@ Creates a config using standard input or from a file for the config content.
For detailed information about using configs, refer to [store configuration data using Docker Configs](https://docs.docker.com/engine/swarm/configs/).
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a Swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -25,8 +25,7 @@ describes all the details of the format.
For detailed information about using configs, refer to [store configuration data using Docker Configs](https://docs.docker.com/engine/swarm/configs/).
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a Swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -24,8 +24,7 @@ Run this command on a manager node to list the configs in the Swarm.
For detailed information about using configs, refer to [store configuration data using Docker Configs](https://docs.docker.com/engine/swarm/configs/).
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a Swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -16,8 +16,7 @@ Removes the specified configs from the Swarm.
For detailed information about using configs, refer to [store configuration data using Docker Configs](https://docs.docker.com/engine/swarm/configs/).
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a Swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the
@ -32,8 +31,7 @@ $ docker config rm my_config
sapth4csdo5b6wz2p5uimh5xg
```
> **Warning**
>
> [!WARNING]
> This command doesn't ask for confirmation before removing a config.
{ .warning }

View File

@ -25,8 +25,7 @@ Use `docker attach` to attach your terminal's standard input, output, and error
ID or name. This lets you view its output or control it interactively, as
though the commands were running directly in your terminal.
> **Note**
>
> [!NOTE]
> The `attach` command displays the output of the container's `ENTRYPOINT` and
> `CMD` process. This can appear as if the attach command is hung when in fact
> the process may simply not be writing any output at that time.
@ -39,8 +38,7 @@ container. If `--sig-proxy` is true (the default),`CTRL-c` sends a `SIGINT` to
the container. If the container was run with `-i` and `-t`, you can detach from
a container and leave it running using the `CTRL-p CTRL-q` key sequence.
> **Note**
>
> [!NOTE]
> A process running as PID 1 inside a container is treated specially by
> Linux: it ignores any signal with the default action. So, the process
> doesn't terminate on `SIGINT` or `SIGTERM` unless it's coded to do so.
@ -97,7 +95,7 @@ CONTAINER ID IMAGE COMMAND CREATED STATUS
Repeating the example above, but this time with the `-i` and `-t` options set;
```console
$ docker run -dit --name topdemo2 ubuntu:22.04 /usr/bin/top -b
$ docker run -dit --name topdemo2 alpine /usr/bin/top -b
```
Now, when attaching to the container, and pressing the `CTRL-p CTRL-q` ("read

View File

@ -44,8 +44,8 @@ created. Supported `Dockerfile` instructions:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c3f279d17e0a ubuntu:22.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:22.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
c3f279d17e0a ubuntu:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
$ docker commit c3f279d17e0a svendowideit/testimage:version3
@ -63,8 +63,8 @@ svendowideit/testimage version3 f5283438590d 16 sec
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c3f279d17e0a ubuntu:22.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:22.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
c3f279d17e0a ubuntu:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
$ docker inspect -f "{{ .Config.Env }}" c3f279d17e0a
@ -85,8 +85,8 @@ $ docker inspect -f "{{ .Config.Env }}" f5283438590d
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c3f279d17e0a ubuntu:22.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:22.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
c3f279d17e0a ubuntu:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
$ docker commit --change='CMD ["apachectl", "-DFOREGROUND"]' -c "EXPOSE 80" c3f279d17e0a svendowideit/testimage:version4
@ -100,6 +100,6 @@ $ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
89373736e2e7 testimage:version4 "apachectl -DFOREGROU" 3 seconds ago Up 2 seconds 80/tcp distracted_fermat
c3f279d17e0a ubuntu:22.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:22.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
c3f279d17e0a ubuntu:24.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:24.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
```

View File

@ -23,7 +23,7 @@ with the container. If a volume is mounted on top of an existing directory in
the container, `docker export` exports the contents of the underlying
directory, not the contents of the volume.
Refer to [Backup, restore, or migrate data volumes](https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes)
Refer to [Backup, restore, or migrate data volumes](https://docs.docker.com/engine/storage/volumes/#back-up-restore-or-migrate-data-volumes)
in the user guide for examples on exporting data in a volume.
## Examples

View File

@ -33,8 +33,7 @@ set through `--signal` may be non-terminal, depending on the container's main
process. For example, the `SIGHUP` signal in most cases will be non-terminal,
and the container will continue running after receiving the signal.
> **Note**
>
> [!NOTE]
> `ENTRYPOINT` and `CMD` in the *shell* form run as a child process of
> `/bin/sh -c`, which does not pass signals. This means that the executable is
> not the containers PID 1 and does not receive Unix signals.

View File

@ -26,7 +26,7 @@ Fetch the logs of a container
The `docker logs` command batch-retrieves logs present at the time of execution.
For more information about selecting and configuring logging drivers, refer to
[Configure logging drivers](https://docs.docker.com/config/containers/logging/configure/).
[Configure logging drivers](https://docs.docker.com/engine/logging/configure/).
The `docker logs --follow` command will continue streaming the new output from
the container's `STDOUT` and `STDERR`.

View File

@ -33,7 +33,7 @@ Running `docker ps --no-trunc` showing 2 linked containers.
$ docker ps --no-trunc
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ca5534a51dd04bbcebe9b23ba05f389466cf0c190f1f8f182d7eea92a9671d00 ubuntu:22.04 bash 17 seconds ago Up 16 seconds 3300-3310/tcp webapp
ca5534a51dd04bbcebe9b23ba05f389466cf0c190f1f8f182d7eea92a9671d00 ubuntu:24.04 bash 17 seconds ago Up 16 seconds 3300-3310/tcp webapp
9ca9747b233100676a48cc7806131586213fa5dab86dd1972d6a8732e3a84a4d crosbymichael/redis:latest /redis-server --dir 33 minutes ago Up 33 minutes 6379/tcp redis,webapp/db
```
@ -64,7 +64,7 @@ e90b8831a4b8 nginx "/bin/bash -c 'mkdir " 11 weeks ago Up 4 hours
* The "size" information shows the amount of data (on disk) that is used for the _writable_ layer of each container
* The "virtual size" is the total amount of disk-space used for the read-only _image_ data used by the container and the writable layer.
For more information, refer to the [container size on disk](https://docs.docker.com/storage/storagedriver/#container-size-on-disk) section.
For more information, refer to the [container size on disk](https://docs.docker.com/engine/storage/drivers/#container-size-on-disk) section.
### <a name="filter"></a> Filtering (--filter)
@ -240,13 +240,13 @@ CONTAINER ID IMAGE COMMAND CREATED
919e1179bdb8 ubuntu-c1 "top" About a minute ago Up About a minute admiring_lovelace
```
Match containers based on the `ubuntu` version `22.04` image:
Match containers based on the `ubuntu` version `24.04` image:
```console
$ docker ps --filter ancestor=ubuntu:22.04
$ docker ps --filter ancestor=ubuntu:24.04
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
82a598284012 ubuntu:22.04 "top" 3 minutes ago Up 3 minutes sleepy_bose
82a598284012 ubuntu:24.04 "top" 3 minutes ago Up 3 minutes sleepy_bose
```
The following matches containers based on the layer `d0e008c6cf02` or an image
@ -256,7 +256,7 @@ that have this layer in its layer stack.
$ docker ps --filter ancestor=d0e008c6cf02
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
82a598284012 ubuntu:22.04 "top" 3 minutes ago Up 3 minutes sleepy_bose
82a598284012 ubuntu:24.04 "top" 3 minutes ago Up 3 minutes sleepy_bose
```
#### Create time

View File

@ -291,8 +291,7 @@ running processes in that namespace. By default, all containers, including
those with `--network=host`, have their own UTS namespace. Setting `--uts` to
`host` results in the container using the same UTS namespace as the host.
> **Note**
>
> [!NOTE]
> Docker disallows combining the `--hostname` and `--domainname` flags with
> `--uts=host`. This is to prevent containers running in the host's UTS
> namespace from attempting to change the hosts' configuration.
@ -350,8 +349,7 @@ In other words, the container can then do almost everything that the host can
do. This flag exists to allow special use-cases, like running Docker within
Docker.
> **Warning**
>
> [!WARNING]
> Use the `--privileged` flag with caution.
> A container with `--privileged` is not a securely sandboxed process.
> Containers in this mode can get a root shell on the host
@ -363,7 +361,7 @@ Docker.
> for example by adding individual kernel capabilities with `--cap-add`.
>
> For more information, see
> [Runtime privilege and Linux capabilities](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities)
> [Runtime privilege and Linux capabilities](https://docs.docker.com/engine/containers/run/#runtime-privilege-and-linux-capabilities)
{ .warning }
The following example doesn't work, because by default, Docker drops most
@ -533,8 +531,7 @@ host. You can also specify `udp` and `sctp` ports. The [Networking overview
page](https://docs.docker.com/network/) explains in detail how to publish ports
with Docker.
> **Note**
>
> [!NOTE]
> If you don't specify an IP address (i.e., `-p 80:80` instead of `-p
> 127.0.0.1:80:80`) when publishing a container's ports, Docker publishes the
> port on all interfaces (address `0.0.0.0`) by default. These ports are
@ -715,8 +712,7 @@ or name. For `overlay` networks or custom plugins that support multi-host
connectivity, containers connected to the same multi-host network but launched
from different Engines can also communicate in this way.
> **Note**
>
> [!NOTE]
> The default bridge network only allows containers to communicate with each other using
> internal IP addresses. User-created bridge networks provide DNS resolution between
> containers using container names.
@ -784,8 +780,7 @@ $ docker network create --subnet 192.0.2.0/24 my-net
$ docker run -itd --network=name=my-net,\"driver-opt=com.docker.network.endpoint.sysctls=net.ipv4.conf.IFNAME.log_martians=1,net.ipv4.conf.IFNAME.forwarding=0\",ip=192.0.2.42 busybox
```
> **Note**
>
> [!NOTE]
> Network drivers may restrict the sysctl settings that can be modified and, to protect
> the operation of the network, new restrictions may be added in the future.
@ -912,8 +907,7 @@ $ docker run --device=/dev/sda:/dev/xvdc:m --rm -it ubuntu fdisk /dev/xvdc
fdisk: unable to open /dev/xvdc: Operation not permitted
```
> **Note**
>
> [!NOTE]
> The `--device` option cannot be safely used with ephemeral devices. You shouldn't
> add block devices that may be removed to untrusted containers with `--device`.
@ -935,15 +929,13 @@ ports on the host visible in the container.
PS C:\> docker run --device=class/86E0D1E0-8089-11D0-9CE4-08003E301F73 mcr.microsoft.com/windows/servercore:ltsc2019
```
> **Note**
>
> [!NOTE]
> The `--device` option is only supported on process-isolated Windows containers,
> and produces an error if the container isolation is `hyperv`.
#### CDI devices
> **Note**
>
> [!NOTE]
> The CDI feature is experimental, and potentially subject to change.
> CDI is currently only supported for Linux containers.
@ -1010,8 +1002,7 @@ ID once the container has finished running.
$ cat somefile | docker run -i -a stdin mybuilder dobuild
```
> **Note**
>
> [!NOTE]
> A process running as PID 1 inside a container is treated specially by
> Linux: it ignores any signal with the default action. So, the process
> doesn't terminate on `SIGINT` or `SIGTERM` unless it's coded to do so.
@ -1124,7 +1115,8 @@ $ docker run -d --device-cgroup-rule='c 42:* rmw' --name my-container my-image
Then, a user could ask `udev` to execute a script that would `docker exec my-container mknod newDevX c 42 <minor>`
the required device when it is added.
> **Note**: You still need to explicitly add initially present devices to the
> [!NOTE]
> You still need to explicitly add initially present devices to the
> `docker run` / `docker create` command.
### <a name="gpus"></a> Access an NVIDIA GPU
@ -1132,8 +1124,7 @@ the required device when it is added.
The `--gpus` flag allows you to access NVIDIA GPU resources. First you need to
install the [nvidia-container-runtime](https://nvidia.github.io/nvidia-container-runtime/).
> **Note**
>
> [!NOTE]
> You can also specify a GPU as a CDI device with the `--device` flag, see
> [CDI devices](#cdi-devices).
@ -1246,8 +1237,7 @@ the container and remove the file system when the container exits, use the
--rm=false: Automatically remove the container when it exits
```
> **Note**
>
> [!NOTE]
> If you set the `--rm` flag, Docker also removes the anonymous volumes
> associated with the container when the container is removed. This is similar
> to running `docker rm -v my-container`. Only volumes that are specified
@ -1322,7 +1312,7 @@ the `--log-driver=<DRIVER>` with the `docker run` command to configure the
container's logging driver.
To learn about the supported logging drivers and how to use them, refer to
[Configure logging drivers](https://docs.docker.com/config/containers/logging/configure/).
[Configure logging drivers](https://docs.docker.com/engine/logging/configure/).
To disable logging for a container, set the `--log-driver` flag to `none`:
@ -1345,14 +1335,12 @@ $ docker run --ulimit nofile=1024:1024 --rm debian sh -c "ulimit -n"
1024
```
> **Note**
>
> [!NOTE]
> If you don't provide a hard limit value, Docker uses the soft limit value
> for both values. If you don't provide any values, they are inherited from
> the default `ulimits` set on the daemon.
> **Note**
>
> [!NOTE]
> The `as` option is deprecated.
> In other words, the following script is not supported:
>
@ -1417,8 +1405,7 @@ the same content between containers.
$ docker run --security-opt label=level:s0:c100,c200 -it fedora bash
```
> **Note**
>
> [!NOTE]
> Automatic translation of MLS labels isn't supported.
To disable the security labeling for a container entirely, you can use
@ -1436,8 +1423,7 @@ that's only allowed to listen on Apache ports:
$ docker run --security-opt label=type:svirt_apache_t -it ubuntu bash
```
> **Note**
>
> [!NOTE]
> You would have to write policy defining a `svirt_apache_t` type.
To prevent your container processes from gaining additional privileges, you can
@ -1558,8 +1544,7 @@ network namespace, run this command:
$ docker run --sysctl net.ipv4.ip_forward=1 someimage
```
> **Note**
>
> [!NOTE]
> Not all sysctls are namespaced. Docker does not support changing sysctls
> inside of a container that also modify the host system. As the kernel
> evolves we expect to see more sysctls become namespaced.

View File

@ -29,8 +29,7 @@ containers do not return any data.
If you need more detailed information about a container's resource usage, use
the `/containers/(id)/stats` API endpoint.
> **Note**
>
> [!NOTE]
> On Linux, the Docker CLI reports memory usage by subtracting cache usage from
> the total memory usage. The API does not perform such a calculation but rather
> provides the total memory usage and the amount from the cache so that clients
@ -41,8 +40,7 @@ the `/containers/(id)/stats` API endpoint.
> field. On cgroup v2 hosts, the cache usage is defined as the value of
> `inactive_file` field.
> **Note**
>
> [!NOTE]
> The `PIDS` column contains the number of processes and kernel threads created
> by that container. Threads is the term used by Linux kernel. Other equivalent
> terms are "lightweight process" or "kernel task", etc. A large number in the

View File

@ -42,8 +42,7 @@ options on a running or a stopped container. On kernel version older than
4.6, you can only update `--kernel-memory` on a stopped container or on
a running container with kernel memory initialized.
> **Warning**
>
> [!WARNING]
> The `docker update` and `docker container update` commands are not supported
> for Windows containers.
{ .warning }
@ -78,8 +77,7 @@ running container only if the container was started with `--kernel-memory`.
If the container was started without `--kernel-memory` you need to stop
the container before updating kernel memory.
> **Note**
>
> [!NOTE]
> The `--kernel-memory` option has been deprecated since Docker 20.10.
For example, if you started a container with this command:

View File

@ -10,8 +10,7 @@ Block until one or more containers stop, then print their exit codes
<!---MARKER_GEN_END-->
> **Note**
>
> [!NOTE]
> `docker wait` returns `0` when run against a container which had already
> exited before the `docker wait` command was run.

View File

@ -186,8 +186,7 @@ Sometimes, multiple options can call for a more complex value string as for
$ docker run -v /host:/container example/mysql
```
> **Note**
>
> [!NOTE]
> Do not use the `-t` and `-a stderr` options together due to
> limitations in the `pty` implementation. All `stderr` in `pty` mode
> simply goes to `stdout`.
@ -247,8 +246,7 @@ By default, configuration file is stored in `~/.docker/config.json`. Refer to th
[change the `.docker` directory](#change-the-docker-directory) section to use a
different location.
> **Warning**
>
> [!WARNING]
> The configuration file and other files inside the `~/.docker` configuration
> directory may contain sensitive information, such as authentication information
> for proxies or, depending on your credential store, credentials for your image
@ -321,11 +319,10 @@ be set for each environment:
These settings are used to configure proxy settings for containers only, and not
used as proxy settings for the `docker` CLI or the `dockerd` daemon. Refer to the
[environment variables](#environment-variables) and [HTTP/HTTPS proxy](https://docs.docker.com/config/daemon/systemd/#httphttps-proxy)
sections for configuring proxy settings for the cli and daemon.
[environment variables](#environment-variables) and [HTTP/HTTPS proxy](https://docs.docker.com/engine/daemon/proxy/#httphttps-proxy)
sections for configuring proxy settings for the CLI and daemon.
> **Warning**
>
> [!WARNING]
> Proxy settings may contain sensitive information (for example, if the proxy
> requires authentication). Environment variables are stored as plain text in
> the container's configuration, and as such can be inspected through the remote
@ -464,8 +461,7 @@ daemon with IP address `174.17.0.1`, listening on port `2376`:
$ docker -H tcp://174.17.0.1:2376 ps
```
> **Note**
>
> [!NOTE]
> By convention, the Docker daemon uses port `2376` for secure TLS connections,
> and port `2375` for insecure, non-TLS connections.

View File

@ -47,8 +47,7 @@ Build an image from a Dockerfile
## Description
> **Note**
>
> [!NOTE]
> This page refers to the **legacy implementation** of `docker build`,
> using the legacy (pre-BuildKit) build backend.
> This configuration is only relevant if you're building Windows containers.
@ -93,7 +92,7 @@ in the context.
When using the legacy builder, it's therefore extra important that you
carefully consider what files you include in the context you specify. Use a
[`.dockerignore`](https://docs.docker.com/build/building/context/#dockerignore-files)
[`.dockerignore`](https://docs.docker.com/build/concepts/context/#dockerignore-files)
file to exclude files and directories that you don't require in your build from
being sent as part of the build context.
@ -146,7 +145,7 @@ the `credentialspec` option. The `credentialspec` must be in the format
#### Overview
> **Note**
> [!NOTE]
> The `--squash` option is an experimental feature, and should not be considered
> stable.

View File

@ -17,6 +17,7 @@ List images
| [`--format`](#format) | `string` | | Format output using a custom template:<br>'table': Print output in table format with column headers (default)<br>'table TEMPLATE': Print output in table format using the given Go template<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
| [`--no-trunc`](#no-trunc) | `bool` | | Don't truncate output |
| `-q`, `--quiet` | `bool` | | Only show image IDs |
| `--tree` | `bool` | | List multi-platform images as a tree (EXPERIMENTAL) |
<!---MARKER_GEN_END-->

View File

@ -82,6 +82,7 @@ which removes images with the specified labels. The other
format is the `label!=...` (`label!=<key>` or `label!=<key>=<value>`), which removes
images without the specified labels.
> [!NOTE]
> **Predicting what will be removed**
>
> If you are using positive filtering (testing for the existence of a label or
@ -186,8 +187,7 @@ This example removes images which have a maintainer label not set to `john`:
$ docker image prune --filter="label!=maintainer=john"
```
> **Note**
>
> [!NOTE]
> You are prompted for confirmation before the `prune` removes
> anything, but you are not shown a list of what will potentially be removed.
> In addition, `docker image ls` doesn't support negative filtering, so it

View File

@ -99,7 +99,7 @@ the same image tagged with different names. Because they are the same image,
their layers are stored only once and do not consume extra disk space.
For more information about images, layers, and the content-addressable store,
refer to [understand images, containers, and storage drivers](https://docs.docker.com/storage/storagedriver/).
refer to [understand images, containers, and storage drivers](https://docs.docker.com/engine/storage/drivers/).
### Pull an image by digest (immutable identifier)
@ -107,8 +107,8 @@ refer to [understand images, containers, and storage drivers](https://docs.docke
So far, you've pulled images by their name (and "tag"). Using names and tags is
a convenient way to work with images. When using tags, you can `docker pull` an
image again to make sure you have the most up-to-date version of that image.
For example, `docker pull ubuntu:22.04` pulls the latest version of the Ubuntu
22.04 image.
For example, `docker pull ubuntu:24.04` pulls the latest version of the Ubuntu
24.04 image.
In some cases you don't want images to be updated to newer versions, but prefer
to use a fixed version of an image. Docker enables you to pull an image by its
@ -117,23 +117,23 @@ of an image to pull. Doing so, allows you to "pin" an image to that version,
and guarantee that the image you're using is always the same.
To know the digest of an image, pull the image first. Let's pull the latest
`ubuntu:22.04` image from Docker Hub:
`ubuntu:24.04` image from Docker Hub:
```console
$ docker pull ubuntu:22.04
$ docker pull ubuntu:24.04
22.04: Pulling from library/ubuntu
24.04: Pulling from library/ubuntu
125a6e411906: Pull complete
Digest: sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d
Status: Downloaded newer image for ubuntu:22.04
docker.io/library/ubuntu:22.04
Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
Status: Downloaded newer image for ubuntu:24.04
docker.io/library/ubuntu:24.04
```
Docker prints the digest of the image after the pull has finished. In the example
above, the digest of the image is:
```console
sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d
sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
```
Docker also prints the digest of an image when pushing to a registry. This
@ -143,23 +143,22 @@ A digest takes the place of the tag when pulling an image, for example, to
pull the above image by digest, run the following command:
```console
$ docker pull ubuntu@sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d
$ docker pull ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
docker.io/library/ubuntu@sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d: Pulling from library/ubuntu
Digest: sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d
Status: Image is up to date for ubuntu@sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d
docker.io/library/ubuntu@sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d
docker.io/library/ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30: Pulling from library/ubuntu
Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
Status: Image is up to date for ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
docker.io/library/ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
```
Digest can also be used in the `FROM` of a Dockerfile, for example:
```dockerfile
FROM ubuntu@sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d
FROM ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
LABEL org.opencontainers.image.authors="some maintainer <maintainer@example.com>"
```
> **Note**
>
> [!NOTE]
> Using this feature "pins" an image to a specific version in time.
> Docker does therefore not pull updated versions of an image, which may include
> security updates. If you want to pull an updated image, you need to change the
@ -215,13 +214,11 @@ shorthand) to see the images that were pulled. The example below shows all the
```console
$ docker image ls --filter reference=ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 18.04 c6ad7e71ba7d 5 weeks ago 63.2MB
ubuntu bionic c6ad7e71ba7d 5 weeks ago 63.2MB
ubuntu 22.04 5ccefbfc0416 2 months ago 78MB
ubuntu focal ff0fea8310f3 2 months ago 72.8MB
ubuntu latest ff0fea8310f3 2 months ago 72.8MB
ubuntu jammy 41ba606c8ab9 3 months ago 79MB
ubuntu 20.04 ba6acccedd29 7 months ago 72.8MB
ubuntu 22.04 8a3cdc4d1ad3 3 weeks ago 77.9MB
ubuntu jammy 8a3cdc4d1ad3 3 weeks ago 77.9MB
ubuntu 24.04 35a88802559d 6 weeks ago 78.1MB
ubuntu latest 35a88802559d 6 weeks ago 78.1MB
ubuntu noble 35a88802559d 6 weeks ago 78.1MB
```
### Cancel a pull

View File

@ -17,6 +17,7 @@ List images
| `--format` | `string` | | Format output using a custom template:<br>'table': Print output in table format with column headers (default)<br>'table TEMPLATE': Print output in table format using the given Go template<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
| `--no-trunc` | `bool` | | Don't truncate output |
| `-q`, `--quiet` | `bool` | | Only show image IDs |
| `--tree` | `bool` | | List multi-platform images as a tree (EXPERIMENTAL) |
<!---MARKER_GEN_END-->

View File

@ -284,8 +284,7 @@ $ docker manifest create --insecure myprivateregistry.mycompany.com/repo/image:1
$ docker manifest push --insecure myprivateregistry.mycompany.com/repo/image:tag
```
> **Note**
>
> [!NOTE]
> The `--insecure` flag is not required to annotate a manifest list,
> since annotations are to a locally-stored copy of a manifest list. You may also
> skip the `--insecure` flag if you are performing a `docker manifest inspect`

View File

@ -80,8 +80,7 @@ sets `net.ipv4.conf.eth3.log_martians=1` and `net.ipv4.conf.eth3.forwarding=0`.
$ docker network connect --driver-opt=\"com.docker.network.endpoint.sysctls=net.ipv4.conf.IFNAME.log_martians=1,net.ipv4.conf.IFNAME.forwarding=0\" multi-host-network container2
```
> **Note**
>
> [!NOTE]
> Network drivers may restrict the sysctl settings that can be modified and, to protect
> the operation of the network, new restrictions may be added in the future.

View File

@ -10,8 +10,7 @@ Demote one or more nodes from manager in the swarm
Demotes an existing manager so that it is no longer a manager.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the [Swarm mode
> section](https://docs.docker.com/engine/swarm/) in the documentation.

View File

@ -21,8 +21,7 @@ given template for each result. Go's
[text/template](https://pkg.go.dev/text/template) package describes all the
details of the format.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -24,8 +24,7 @@ Lists all the nodes that the Docker Swarm manager knows about. You can filter
using the `-f` or `--filter` flag. Refer to the [filtering](#filter) section
for more information about available filter options.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the
@ -42,8 +41,7 @@ ID HOSTNAME STATUS AVAILABILITY MANAGER STATU
e216jshn25ckzbvmwlnh5jr3g * swarm-manager1 Ready Active Leader
```
> **Note**
>
> [!NOTE]
> In the above example output, there is a hidden column of `.Self` that indicates
> if the node is the same node as the current docker daemon. A `*` (e.g.,
> `e216jshn25ckzbvmwlnh5jr3g *`) means this node is the current docker daemon.

View File

@ -10,8 +10,7 @@ Promote one or more nodes to manager in the swarm
Promotes a node to manager. This command can only be executed on a manager node.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -22,8 +22,7 @@ Lists all the tasks on a Node that Docker knows about. You can filter using the
`-f` or `--filter` flag. Refer to the [filtering](#filter) section for more
information about available filter options.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -20,8 +20,7 @@ Remove one or more nodes from the swarm
Removes the specified nodes from a swarm.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -19,8 +19,7 @@ Update a node
Update metadata about a node, such as its availability, labels, or roles.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -100,8 +100,7 @@ $ docker plugin inspect -f '{{with $mount := index .Settings.Mounts 0}}{{$mount.
/bar
```
> **Note**
>
> [!NOTE]
> Since only `source` is settable in `mymount`,
> `docker plugins set mymount=/bar myplugin` would work too.
@ -122,8 +121,7 @@ $ docker plugin inspect -f '{{with $device := index .Settings.Devices 0}}{{$devi
/dev/bar
```
> **Note**
>
> [!NOTE]
> Since only `path` is settable in `mydevice`,
> `docker plugins set mydevice=/dev/bar myplugin` would work too.

View File

@ -20,8 +20,7 @@ Creates a secret using standard input or from a file for the secret content.
For detailed information about using secrets, refer to [manage sensitive data with Docker secrets](https://docs.docker.com/engine/swarm/secrets/).
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -25,8 +25,7 @@ describes all the details of the format.
For detailed information about using secrets, refer to [manage sensitive data with Docker secrets](https://docs.docker.com/engine/swarm/secrets/).
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -24,8 +24,7 @@ Run this command on a manager node to list the secrets in the swarm.
For detailed information about using secrets, refer to [manage sensitive data with Docker secrets](https://docs.docker.com/engine/swarm/secrets/).
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -16,8 +16,7 @@ Removes the specified secrets from the swarm.
For detailed information about using secrets, refer to [manage sensitive data with Docker secrets](https://docs.docker.com/engine/swarm/secrets/).
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the
@ -32,8 +31,7 @@ $ docker secret rm secret.json
sapth4csdo5b6wz2p5uimh5xg
```
> **Warning**
>
> [!WARNING]
> Unlike `docker rm`, this command does not ask for confirmation before removing
> a secret.
{ .warning }

View File

@ -25,8 +25,7 @@ Manage Swarm services
Manage Swarm services.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -88,8 +88,7 @@ Create a new service
Creates a service as described by the specified parameters.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the
@ -699,7 +698,7 @@ follows:
| `node.platform.os` | Node operating system | `node.platform.os==windows` |
| `node.platform.arch` | Node architecture | `node.platform.arch==x86_64` |
| `node.labels` | User-defined node labels | `node.labels.security==high` |
| `engine.labels` | Docker Engine's labels | `engine.labels.operatingsystem==ubuntu-22.04` |
| `engine.labels` | Docker Engine's labels | `engine.labels.operatingsystem==ubuntu-24.04` |
`engine.labels` apply to Docker Engine labels like operating system, drivers,
etc. Swarm administrators add `node.labels` for operational purposes by using
@ -941,7 +940,7 @@ $ docker service create \
The swarm extends my-network to each node running the service.
Containers on the same network can access each other using
[service discovery](https://docs.docker.com/network/drivers/overlay/#container-discovery).
[service discovery](https://docs.docker.com/engine/network/drivers/overlay/#container-discovery).
Long form syntax of `--network` allows to specify list of aliases and driver options:
`--network name=my-network,alias=web1,driver-opt=field1=value1`

View File

@ -23,8 +23,7 @@ the given template will be executed for each result.
Go's [text/template](https://pkg.go.dev/text/template) package
describes all the details of the format.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -24,8 +24,7 @@ Fetch the logs of a service or task
The `docker service logs` command batch-retrieves logs present at the time of execution.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the
@ -36,13 +35,12 @@ service, or with the ID of a task. If a service is passed, it will display logs
for all of the containers in that service. If a task is passed, it will only
display logs from that particular task.
> **Note**
>
> [!NOTE]
> This command is only functional for services that are started with
> the `json-file` or `journald` logging driver.
For more information about selecting and configuring logging drivers, refer to
[Configure logging drivers](https://docs.docker.com/config/containers/logging/configure/).
[Configure logging drivers](https://docs.docker.com/engine/logging/configure/).
The `docker service logs --follow` command will continue streaming the new output from
the service's `STDOUT` and `STDERR`.

View File

@ -22,8 +22,7 @@ List services
This command lists services that are running in the swarm.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -20,8 +20,7 @@ List the tasks of one or more services
Lists the tasks that are running as part of the specified services.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -14,8 +14,7 @@ Remove one or more services
Removes the specified services from the swarm.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the
@ -35,8 +34,7 @@ $ docker service ls
ID NAME MODE REPLICAS IMAGE
```
> **Warning**
>
> [!WARNING]
> Unlike `docker rm`, this command does not ask for confirmation before removing
> a running service.

View File

@ -17,8 +17,7 @@ Revert changes to a service's configuration
Roll back a specified service to its previous version from the swarm.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -20,8 +20,7 @@ services which are global mode. The command will return immediately, but the
actual scaling of the service may take some time. To stop all replicas of a
service while keeping the service active in the swarm you can set the scale to 0.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -115,8 +115,7 @@ service requires recreating the tasks for it to take effect. For example, only c
setting. However, the `--force` flag will cause the tasks to be recreated anyway. This can be used to perform a
rolling restart without any changes to the service parameters.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -25,8 +25,7 @@ Deploy a new stack or update an existing stack
Create and update a stack from a `compose` file on the swarm.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -20,8 +20,7 @@ List stacks
Lists the stacks.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -20,8 +20,7 @@ List the tasks in the stack
Lists the tasks that are running as part of the specified stack.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -20,8 +20,7 @@ Remove one or more stacks
Remove the stack from the swarm.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -18,8 +18,7 @@ List the services in the stack
Lists the services that are running as part of the specified stack.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -22,8 +22,7 @@ Display and rotate the root CA
View or rotate the current swarm CA certificate.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the
@ -81,8 +80,7 @@ gyg5u9Iliel99l7SuMhNeLkrU7fXs+Of1nTyyM73ig==
### <a name="rotate"></a> Root CA rotation (--rotate)
> **Note**
>
> [!NOTE]
> Mirantis Kubernetes Engine (MKE), formerly known as Docker UCP, provides an external
> certificate manager service for the swarm. If you run swarm on MKE, you shouldn't
> rotate the CA certificates manually. Instead, contact Mirantis support if you need

View File

@ -21,8 +21,7 @@ role. You pass the token using the `--token` flag when you run
[swarm join](swarm_join.md). Nodes use the join token only when they join the
swarm.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -22,8 +22,7 @@ swarm.
You can view or rotate the unlock key using `swarm unlock-key`. To view the key,
run the `docker swarm unlock-key` command without any arguments:
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -13,8 +13,7 @@ used to reactivate a manager after its Docker daemon restarts if the autolock
setting is turned on. The unlock key is printed at the time when autolock is
enabled, and is also available from the `docker swarm unlock-key` command.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

View File

@ -22,8 +22,7 @@ Update the swarm
Updates a swarm with new parameter values.
> **Note**
>
> [!NOTE]
> This is a cluster management command, and must be executed on a swarm
> manager node. To learn about managers and workers, refer to the
> [Swarm mode section](https://docs.docker.com/engine/swarm/) in the

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