Compare commits

..

197 Commits

Author SHA1 Message Date
d8eb465f86 Merge pull request #6424 from vvoland/update-docker
Some checks failed
build / bin-image (push) Has been cancelled
build / prepare-plugins (push) Has been cancelled
build / plugins (push) Has been cancelled
codeql / codeql (push) Has been cancelled
e2e / tests (alpine, 23, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 23, local) (push) Has been cancelled
e2e / tests (alpine, 26, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 26, local) (push) Has been cancelled
e2e / tests (alpine, 27, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 27, local) (push) Has been cancelled
e2e / tests (alpine, 28, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 28, local) (push) Has been cancelled
e2e / tests (debian, 23, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 23, local) (push) Has been cancelled
e2e / tests (debian, 26, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 26, local) (push) Has been cancelled
e2e / tests (debian, 27, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 27, local) (push) Has been cancelled
e2e / tests (debian, 28, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 28, local) (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
[28.x] vendor: github.com/docker/docker v28.4.0-dev (249d679a6baf)
2025-09-03 22:52:20 +02:00
a83e3df40b vendor: github.com/docker/docker v28.4.0-dev (249d679a6baf)
full diff: https://github.com/docker/docker/compare/v28.4.0-rc.2...249d679a6baf

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2025-09-03 22:32:15 +02:00
ff5ea757b4 Merge pull request #6422 from vvoland/6421-28.x
[28.x backport] update to go1.24.7
2025-09-03 21:28:57 +02:00
c1d14aef3c Merge pull request #6423 from vvoland/update-docker
[28.x] vendor: github.com/docker/docker v28.4.0-rc.2
2025-09-03 21:04:36 +02:00
18adfd54a9 vendor: github.com/docker/docker v28.4.0-rc.2
full diff: https://github.com/docker/docker/compare/5d5332b00c76...v28.4.0-rc.2

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2025-09-03 20:58:26 +02:00
6e20b9d93c update to go1.24.7
This includes 1 security fix:

- net/http: CrossOriginProtection bypass patterns are over-broad

    When passing patterns to CrossOriginProtection.AddInsecureBypassPattern,
    requests that would have redirected to those patterns (e.g. without a trailing
    slash) were also exempted, which might be unexpected.

    Thanks to Marco Gazerro for reporting this issue.

    This is CVE-2025-47910 and Go issue https://go.dev/issue/75054.

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

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit f64b8a332d)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2025-09-03 20:44:05 +02:00
3cadbd82c5 Merge pull request #6420 from thaJeztah/28.x_backport_complete_pull
[28.x backport] add completion for docker image pull
2025-09-03 17:35:07 +02:00
b3df92053d add completion for docker image pull
With this patch, completion is provided for images already present
in the local image cache to help pulling the latest version of the
same tag;

    docker pull go<tab>
    golang:1.12    golang:1.18.0  golang:1.21    golang:1.24    gopher:latest
    golang:1.13    golang:1.20    golang:1.23    golang:latest

    docker pull golang:<tab>
    1.12    1.13    1.18.0  1.20    1.21    1.23    1.24    latest

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 5bf3c6793d)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-03 15:42:07 +02:00
b89b069082 Merge pull request #6416 from vvoland/update-docker
Some checks failed
build / bin-image (push) Has been cancelled
build / prepare-plugins (push) Has been cancelled
build / plugins (push) Has been cancelled
codeql / codeql (push) Has been cancelled
e2e / tests (alpine, 23, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 23, local) (push) Has been cancelled
e2e / tests (alpine, 26, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 26, local) (push) Has been cancelled
e2e / tests (alpine, 27, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 27, local) (push) Has been cancelled
e2e / tests (alpine, 28, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 28, local) (push) Has been cancelled
e2e / tests (debian, 23, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 23, local) (push) Has been cancelled
e2e / tests (debian, 26, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 26, local) (push) Has been cancelled
e2e / tests (debian, 27, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 27, local) (push) Has been cancelled
e2e / tests (debian, 28, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 28, local) (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
[28.x] vendor: github.com/docker/docker v28.4.0-rc.2-dev (5d5332b00c76)
2025-09-02 12:37:23 +02:00
9deb5e9cd3 vendor: github.com/docker/docker v28.4.0-rc.2-dev (5d5332b00c76)
full diff: https://github.com/docker/docker/compare/v28.4.0-rc.1...5d5332b00c76

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2025-09-02 12:15:13 +02:00
e8be20bc9c Merge pull request #6413 from thaJeztah/28.x_backport_deprecate_OauthLoginEscapeHatchEnvVar
[28.x backport] cli/command/registry: deprecate OauthLoginEscapeHatchEnvVar
2025-09-01 18:05:08 +02:00
f2cd1979c0 Merge pull request #6411 from thaJeztah/28.x_backport_deprecate_ReexecEnvvar
[28.x backport] cli-plugins/manager: deprecate ReexecEnvvar
2025-09-01 17:57:49 +02:00
991d942cc3 cli/command/registry: deprecate OauthLoginEscapeHatchEnvVar
This const was added in 846ecf59ff, but
only used internally. This patch deprecates the const, to be removed
in the next release.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 18cdc25bb4)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-01 17:47:37 +02:00
8e49313c0c cli-plugins/manager: deprecate ReexecEnvvar
This alias was added in 4321293972, which is
part of v28.0, but did not deprecate them. They are no longer used in the
CLI itself, but may be used by cli-plugin implementations.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 6fa7d18320)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-01 17:37:27 +02:00
6cd42da462 Merge pull request #6409 from thaJeztah/28.x_bump_engine
[28.x] vendor: github.com/docker/docker v28.4.0-rc.1
2025-09-01 13:14:46 +02:00
2b9489d827 [28.x] vendor: github.com/docker/docker v28.4.0-rc.1
full diff: https://github.com/moby/moby/compare/02b4a1a3decc...v28.4.0-rc.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-01 11:24:20 +02:00
fbeae8516b Merge pull request #6405 from thaJeztah/28.x_backport_deprecate_nocomplete
[28.x backport] cli/command/completion: deprecate NoComplete
2025-09-01 09:13:54 +02:00
d593e61275 Merge pull request #6403 from thaJeztah/28.x_backport_deprecate_context_funcs
[28.x backport] cli/command/context: deprecate exported types and functions
2025-09-01 09:12:27 +02:00
139968dff2 cli/command/completion: deprecate NoComplete
This function was an exact duplicate of [cobra.NoFileCompletions], so
deprecating it in favor of that.

[cobra.NoFileCompletions]: https://pkg.go.dev/github.com/spf13/cobra@v1.9.1#NoFileCompletions

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 2827d037ba)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-30 12:24:11 +02:00
925db59377 cli/command/context: deprecate exported types and functions
These functions and types are shallow wrappers around the context
store and were intended for internal use as implementation for the
CLI itself.

They were exported in 3126920af1 to be
used by plugins and Docker Desktop. However, there's currently no public
uses of this, and Docker Desktop does not use these functions.

This patch deprecates the exported functions as they were meant to be
implementation specific for the CLI. If there's a need to provide
utilities for manipulating the context-store other than through the
CLI itself, we can consider creating an SDK for that purpose.

This deprecates:

- `RunCreate` and `CreateOptions`
- `RunExport` and `ExportOptions`
- `RunImport`
- `RunRemove` and `RemoveOptions`
- `RunUpdate` and `UpdateOptions`
- `RunUse`

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 95eeafa551)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-30 12:13:27 +02:00
aced30d906 Merge pull request #6399 from vvoland/6371-28.x
Some checks failed
build / bin-image (push) Has been cancelled
build / prepare-plugins (push) Has been cancelled
build / plugins (push) Has been cancelled
codeql / codeql (push) Has been cancelled
e2e / tests (alpine, 23, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 23, local) (push) Has been cancelled
e2e / tests (alpine, 26, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 26, local) (push) Has been cancelled
e2e / tests (alpine, 27, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 27, local) (push) Has been cancelled
e2e / tests (alpine, 28, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 28, local) (push) Has been cancelled
e2e / tests (debian, 23, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 23, local) (push) Has been cancelled
e2e / tests (debian, 26, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 26, local) (push) Has been cancelled
e2e / tests (debian, 27, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 27, local) (push) Has been cancelled
e2e / tests (debian, 28, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 28, local) (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
[28.x backport] Add escape hatch for GODEBUG=x509negativeserial
2025-08-29 16:34:50 +02:00
b9e15efde0 return early if GODEBUG set or context is default
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit 72f79333e5)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2025-08-29 16:00:23 +02:00
4108febfae rename function to fit what it is doing
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit 6163c03b11)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2025-08-29 16:00:21 +02:00
8cfe1f712e Test setAllowNegativex509
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit 467305fcea)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2025-08-29 16:00:19 +02:00
7c34fd56d0 Cleanup setAllowNegativex509
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit 65a6c35d90)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2025-08-29 16:00:18 +02:00
0a2eaa4d05 Add escape hatch for GODEBUG=x509negativeserial
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit 7d7a7aac4d)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2025-08-29 16:00:16 +02:00
d26f1fd6d3 Merge pull request #6266 from thaJeztah/28.x_bump_engine
[28.x] vendor: github.com/docker/docker 02b4a1a3decc (v28.4.0-dev)
2025-08-28 16:38:19 +02:00
4f6182a50c Merge pull request #6394 from thaJeztah/28.x_bump_version
[28.x] bump version to v28.4.0-dev
2025-08-28 16:37:58 +02:00
4caa99468d [28.x] bump version to v28.4.0-dev
This file is only used as default if no version is specified. We
should probably get rid of this, but let's update it to better
reflect the version that developer builds are building.

d48fb9f9f7/docker.Makefile (L22)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-28 15:52:47 +02:00
c2117f956f vendor: github.com/docker/docker 02b4a1a3decc (v28.4.0-dev)
full diff: https://github.com/moby/moby/compare/v28.3.3...02b4a1a3decc99de2965421cea5634c1e5297266

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-28 15:51:57 +02:00
19b86ef9e2 Merge pull request #6391 from thaJeztah/28.x_backport_deprecate_stack_commands
[28.x backport] cli/command/stack/*: deprecate exported functions and types
2025-08-28 13:55:57 +02:00
88d6197d4c Merge pull request #6392 from thaJeztah/28.x_backport_fix_email_deprecation
[28.x backport] cli/config/types: update deprecation comment for AuthConfig.Email
2025-08-28 13:52:44 +02:00
bda15c766a cli/config/types: update deprecation comment for AuthConfig.Email
Relates to [cli@27b2797], which forked this type from the Moby API, and
[moby@6cfff7e], which made the same change on the API side.

The Email field was originally used to create a new Docker Hub account
through the `docker login` command. The `docker login` command could be
used both to log in to an existing account (providing only username and
password), or to create a new account (providing desired username and
password, and an e-mail address to use for the new account).

This functionality was confusing, because it was implemented when Docker
Hub was the only registry, but the same functionality could not be used
for other registries. This functionality was removed in Docker 1.11 (API
version 1.23) through [moby@aee260d], which also removed the Email field
([engine-api@9a9e468]) as it was no longer used.

However, this caused issues when using a new CLI connecting with an old
daemon, as the field would no longer be serialized, and the deprecation
may not yet be picked up by custom registries, so [engine-api@167efc7]
added the field back, deprecated it, and added an "omitempty". There
was no official "deprecated" format yet at the time, so let's make sure
the deprecation follows the proper format to make sure it gets noticed.

[cli@27b2797]: 27b2797f7d
[moby@6cfff7e]: 6cfff7e880
[moby@aee260d]: aee260d4eb
[engine-api@9a9e468]: 9a9e468f50
[engine-api@167efc7]: 167efc72bb

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit aab947de8f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-28 13:22:09 +02:00
e636ed2ae5 cli/command/stack: deprecate RunList, RunServices
Functions and types in this package were exported as part of the "compose
on kubernetes" feature, which was deprecated and removed. These functions
are meant for internal use, and will be removed in the next release.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit d16c560664)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-28 12:07:56 +02:00
19b7dfa1cf deprecate cli/command/stack/swarm
Functions and types in this package were exported as part of the "compose
on kubernetes" feature, which was deprecated and removed. These functions
are meant for internal use, and will be removed in the next release.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 036d3a6bab)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-28 12:07:52 +02:00
91de2a6dae deprecate cli/command/stack/options
Functions and types in this package were exported as part of the "compose
on kubernetes" feature, which was deprecated and removed. These functions
are meant for internal use, and will be removed in the next release.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit f0e5a0d654)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-28 12:04:51 +02:00
7c6f58affe deprecate cli/command/stack/loader
Functions and types in this package were exported as part of the "compose
on kubernetes" feature, which was deprecated and removed. These functions
are meant for internal use, and will be removed in the next release.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit ad6ab189a6)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-28 12:04:51 +02:00
8c6a7fcbc9 deprecate cli/command/stack/formatter
Functions and types in this package were exported as part of the "compose
on kubernetes" feature, which was deprecated and removed. These functions
are meant for internal use, and will be removed in the next release.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 30774ed1f2)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-28 12:04:48 +02:00
3e87f59859 Merge pull request #6390 from thaJeztah/28.x_backport_avoid_shadowing
[28.x backport] cli/command: rename vars for consistency and prevent shadowing
2025-08-28 12:03:34 +02:00
a621313658 cli/command: rename vars for consistency and prevent shadowing
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9fd71c8347)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-28 11:37:25 +02:00
69dd67fc06 Merge pull request #6381 from thaJeztah/28.x_backport_internalize_ParseEnvFile
[28.x backport] opts: deprecate ParseEnvFile
2025-08-27 12:26:38 -07:00
b90c5a8fcb Merge pull request #6379 from thaJeztah/28.x_backport_test_fixes
[28.x backport] cli/command/system: don't use deprecated fields in tests
2025-08-27 12:22:48 -07:00
99676e6c9c Merge pull request #6384 from thaJeztah/28.x_backport_mergo
[28.x backport] vendor: dario.cat/mergo v1.0.2
2025-08-27 11:18:09 -07:00
524fd156f5 Merge pull request #6380 from thaJeztah/28.x_backport_docs_fixes
[28.x backport] docs fixes and fix flag annotation
2025-08-27 11:17:14 -07:00
07bcb9cca2 Merge pull request #6386 from thaJeztah/28.x_backport_bump_go_events
[28.x backport] vendor: github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32
2025-08-27 11:14:41 -07:00
48ea3fa14f vendor: github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32
full diff: e31b211e4f...c867878c5e

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 05220c5f19)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-27 15:55:20 +02:00
a27a86289e vendor: dario.cat/mergo v1.0.2
drops gopkg.in/yaml.v3 as dependency

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

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit a93ed48d06)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-27 15:47:11 +02:00
e112eeea4d opts: deprecate ParseEnvFile
It was a wrapper around kvfile.Load, which should be used instead.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit e650803f09)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-27 15:00:30 +02:00
3b68cde77a cli/command/service: fix API annotations for generic resource flags
These flags were added in 20a6ff32ee, and require
API version v1.32 or up, but they accidentally copied the flag-name from another
flag, so were not setting the annotation correctly.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit dcc3d25dc2)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-27 14:57:28 +02:00
c712d97172 docs: add missing backticks in 'run.md'
Signed-off-by: Hossein Abbasi <16090309+hsnabszhdn@users.noreply.github.com>
(cherry picked from commit d9cafa759f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-27 14:57:15 +02:00
f385cb994f docs: fix sentence structures in 'run.md'
Signed-off-by: Hossein Abbasi <16090309+hsnabszhdn@users.noreply.github.com>
(cherry picked from commit ba2c1c94ab)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-27 14:57:00 +02:00
44c1d5775d cli/command/system: don't use deprecated fields in test
This only impacts the JSON marshaled output; the "regular" output
of `docker info` already ignores these fields.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 3d87aa441f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-27 14:51:55 +02:00
1468bb1962 cli/command/system: TestEventsFormat: remove use of deprecated fields
These were just testing JSON marshaling fields that are deprecated, but
may be present in a response; these fields will be removed in future
API versions, so stop testing for them.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 823c6a75b3)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-27 14:49:21 +02:00
44fb305591 Merge pull request #6348 from thaJeztah/28.x_backport_bump_x_sync
[28.x backport] vendor: golang.org/x/sync v0.16.0
2025-08-26 08:26:11 -07:00
b74574cd53 Merge pull request #6369 from thaJeztah/28.x_backport_deprecate_exported_config_funcs
[28.x backport] cli/command/config: deprecate exported types and functions
2025-08-26 13:19:00 +02:00
e60b20f08e cli/command/config: deprecate exported types and functions
These were exported in f60369dfe6 to be
used in docker enterprise, but this never happened, and there's no
known consumers of these, so we should deprecate these. External
consumers can still call the API-client directly, which should've
been the correct thing to do in the first place.

This deprecates:

- `RunConfigCreate` and  `CreateOptions`
- `RunConfigInspect` and `InspectOptions`
- `RunConfigList` and `ListOptions`
- `RunConfigRemove` and `RemoveOptions`

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit a5f4ba08d9)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-25 18:55:48 +02:00
6f7865f89a Merge pull request #6358 from thaJeztah/28.x_backport_rm_image_pull
[28.x backport] cli/command/image: remove exported RunPull, PullOptions
2025-08-25 16:20:31 +02:00
23518a47dd Merge pull request #6357 from thaJeztah/28.x_backport_unexport_authresolver_util
[28.x backport] cli/command/image: deprecate AuthResolver and un-export
2025-08-25 16:20:09 +02:00
eb120d2c28 Merge pull request #6354 from thaJeztah/28.x_backport_rm_json_deprecated
[28.x backport] internal/jsonstream: remove uses of deprecated fields
2025-08-25 15:52:28 +02:00
146f6492c7 cli/command/image: remove exported RunPull, PullOptions
These were exported in 812f113685, but
while the function and options are exported, the option-fields were
all un-exported, so these were not usable.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9216f04eb6)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-23 03:51:47 +02:00
8def226e53 cli/command/image: deprecate AuthResolver and un-export
This function was exported to share it between "trust" and "image",
but was only a shallow wrapper, so split the implementations where
used.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 7ad113ccc2)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-23 03:48:35 +02:00
82c5138c8e internal/jsonstream: remove uses of deprecated fields
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 045ac0b159)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-23 02:18:45 +02:00
bd3e420cc7 vendor: golang.org/x/sync v0.16.0
Brings in the errgroup implementation for reverted auto-recover from panics.

full diff: https://github.com/golang/sync/compare/v0.14.0...v0.16.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit c7cbac58b3)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 21:12:55 +02:00
bb2ed910bb Merge pull request #6343 from thaJeztah/28.x_backport_deprecate_builder_NewPruneCommand
[28.x backport] cli/command/builder: deprecate NewPruneCommand
2025-08-22 12:35:24 +02:00
1415080cfa cli/command/builder: deprecate NewPruneCommand
This patch deprecates exported NewPruneCommand and moves the
implementation details to an unexported function.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 7032f5922e)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 12:22:11 +02:00
0bbdda88e6 Merge pull request #6341 from thaJeztah/28.x_backport_internalize_formatters
[28.x backport] cli/command/*: deprecate formatting-related functions and types
2025-08-22 12:10:03 +02:00
4133271530 cli/command/trust: deprecate formatting-related functions and types
It's part of the presentation logic of the cli, and only used internally.
We can consider providing utilities for these, but better as part of
separate packages.

This deprecates the following types and functions:

- `SignedTagInfo`
- `SignerInfo`
- `NewTrustTagFormat`
- `NewSignerInfoFormat`
- `TagWrite`
- `SignerInfoWrite`

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 95c9b1b13b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 11:47:54 +02:00
0e17907266 cli/command/task: deprecate NewTaskFormat, FormatWrite
It's part of the presentation logic of the cli, and only used internally.
We can consider providing utilities for these, but better as part of
separate packages.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit c3ee82fdc3)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 11:47:54 +02:00
b0b7dcfe86 cli/command/service: deprecate NewFormat, InspectFormatWrite
It's part of the presentation logic of the cli, and only used internally.
We can consider providing utilities for these, but better as part of
separate packages.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9f453d3fea)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 11:47:54 +02:00
531ea22e7d cli/command/secret: deprecate NewFormat, FormatWrite, InspectFormatWrite
It's part of the presentation logic of the cli, and only used internally.
We can consider providing utilities for these, but better as part of
separate packages.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit f3088e37a0)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 11:47:53 +02:00
e8cf8e65f7 cli/command/registry: deprecate NewSearchFormat, SearchWrite
It's part of the presentation logic of the cli, and only used internally.
We can consider providing utilities for these, but better as part of
separate packages.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 83371c2014)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 11:47:53 +02:00
22c68c5366 cli/command/plugin: deprecate NewFormat, FormatWrite
It's part of the presentation logic of the cli, and only used internally.
We can consider providing utilities for these, but better as part of
separate packages.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit bf47419852)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 11:47:53 +02:00
e424c50373 cli/command/node: deprecate NewFormat, FormatWrite, InspectFormatWrite
It's part of the presentation logic of the cli, and only used internally.
We can consider providing utilities for these, but better as part of
separate packages.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 123ef81f7d)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 11:47:53 +02:00
399861e7d2 cli/command/config: deprecate NewFormat, FormatWrite, InspectFormatWrite
It's part of the presentation logic of the cli, and only used internally.
We can consider providing utilities for these, but better as part of
separate packages.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit e626f778ec)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 11:47:52 +02:00
694e92aa10 cli/command/checkpoint: deprecate NewFormat, FormatWrite
It's part of the presentation logic of the cli, and only used internally.
We can consider providing utilities for these, but better as part of
separate packages.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit d861b78a8a)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 11:47:52 +02:00
09a36f2ef1 cli/command/image: deprecate NewHistoryFormat, HistoryWrite
It's part of the presentation logic of the cli, and only used internally.
We can consider providing utilities for these, but better as part of
separate packages.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 15cf4fa912)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 11:47:52 +02:00
73fbe6a020 cli/command/network: deprecate NewFormat, FormatWrite
It's part of the presentation logic of the cli, and only used internally.
We can consider providing utilities for these, but better as part of
separate packages.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit e3903a1ac8)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 11:47:52 +02:00
4480024a80 cli/command/container: deprecate DiffFormatWrite
It's part of the presentation logic of the cli, and only used internally.
We can consider providing utilities for these, but better as part of
separate packages.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit fdc90caeee)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 11:47:52 +02:00
b4f2c0ca3d cli/command/container: newDiffContext: use struct-literal
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 0db7b9f774)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 11:47:51 +02:00
85a5e3da36 cli/command/container: DiffFormatWrite: remove intermediate var
Also rename "ctx" argument; we shouldn't use this as name for things
that are not a context.Context.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 239b727834)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 11:47:51 +02:00
34a75fb6f4 cli/command/container: deprecate NewDiffFormat
It's part of the presentation logic of the cli, and only used internally.
We can consider providing utilities for these, but better as part of
separate packages.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 907507e22a)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 11:47:48 +02:00
b52ab17a14 Merge pull request #6340 from thaJeztah/28.x_backport_cleanup_plugins
[28.x backport] cli/command/plugin: fix linting issues, and assorted cleanups
2025-08-22 11:31:06 +02:00
ec89d0ba67 cli/command/plugin: fix linting issues, and assorted cleanups
- fix various unhandled errors
- remove some locally defined option-types in favor of option-types
  defined by the client / api
- don't use unkeyed structs in tests, and add docs for some subtests
- fix some values in tests that triggered "spellcheck" warnings
- inline vars / functions that only had a single use.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit c6f935eba5)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-22 09:54:50 +02:00
aa44c436d7 Merge pull request #6333 from thaJeztah/28.x_fix_plugin_tests
[28.x] e2e/testutils: fix incorrect use of PluginConfigInterface
2025-08-20 15:29:46 -07:00
39e1213615 e2e/testutils: fix incorrect use of PluginConfigInterface
This code was using the type incorrectly; current versions of the
API MarshalText ignore this mistake, but the moby/moby/api module
produces an error:

    === Failed
    === FAIL: e2e/global TestPromptExitCode/plugin_install (0.28s)
        cli_test.go:203: assertion failed: error is not nil: json: error calling MarshalText for type plugin.CapabilityID: capability "docker.dummy/1.0" cannot contain a dot

    === FAIL: e2e/global TestPromptExitCode/plugin_upgrade (0.26s)
        cli_test.go:203: assertion failed: error is not nil: json: error calling MarshalText for type plugin.CapabilityID: capability "docker.dummy/1.0" cannot contain a dot

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 22:00:06 +02:00
5184f2ed65 Merge pull request #6332 from thaJeztah/28.x_deprecate_prompt_utils
[28.x] cli/command: deprecate prompt utilities that were for internal use
2025-08-20 11:55:06 -07:00
cb42a72704 cli/command: deprecate prompt utilities that were for internal use
- The `DisableInputEcho` and `PromptForInput` utilities were added in
  c15ade0c64 as part of a bug-fix, which
  was part of v28.x. [There are no (publicly visible) users][1] of either.
- The `ErrPromptTerminated` was added in v26.x (originally added in
  10bf91a02d, later updated in commit
  7c722c08d0. [It is not used][2]
- The `PromptForConfirmation` was added in [moby@280c872] (docker v1.13.0)
  as part of the `docker <object> prune` subcommands. It was meant for
  internal use but exported to allow re-using it in the `container`,
  `image` (etc.) packages. However, a breaking change to its signature
  was made in 10bf91a02d. It currently
  does [not appear to have any (public) users][2].

This patch deprecates the `ErrPromptTerminated`, `DisableInputEcho`,
`PromptForInput`, and `PromptForConfirmation` utilities from the
`cli/command` package. The core functionality of these is still
available in the `internal/prompt` package, which we may make
public at some point, but still needs some refining / decoupling.

[moby@280c872]: 280c872366
[1]: https://grep.app/search?f.lang=Go&regexp=true&q=%5C.%28DisableInputEcho%7CPromptForInput%29%5C%28
[2]: https://grep.app/search?f.lang=Go&q=%5C.ErrPromptTerminated
[3]: https://grep.app/search?f.lang=Go&q=.PromptForConfirmation%28

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 18:04:22 +02:00
5ef48d620d Merge pull request #6331 from thaJeztah/28.x_backport_issue_6188
[28.x backport] refactor(cli/compose/loader): extract ParseVolume() to its own package
2025-08-20 17:27:05 +02:00
9e08776681 refactor(cli/compose/loader): extract ParseVolume() to its own package
Moves ParseVolume() to a new internal package to remove the dependency
on cli/compose/loader in cli/command/container/opts.go

refactor to keep types isolated

- rename the package to "volumespec" to reuse the name of the package
  as part of the name (parsevolume.ParseVolume() -> volumespec.Parse())
- move the related compose types to the internal package as well,
  and rename them to be more generic (not associated with "compose");
  - ServiceVolumeConfig -> VolumeConfig
  - ServiceVolumeBind -> BindOpts
  - ServiceVolumeVolume -> VolumeOpts
  - ServiceVolumeImage -> ImageOpts
  - ServiceVolumeTmpfs -> TmpFsOpts
  - ServiceVolumeCluster -> ClusterOpts
- alias the internal types inside cli/compose/types to keep backward
  compatibility (for any external consumers); even though the implementation
  is internal, Go allows aliasing types to use them externally.

Signed-off-by: Michael Tews <michael@tews.dev>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit ef7fd8bb67)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 16:52:12 +02:00
85512e35cd Merge pull request #6312 from thaJeztah/28.x_backport_deprecate_cobra_commands
[28.x backport] un-export and deprecate cobra commands
2025-08-20 15:55:43 +02:00
ce242b0ee8 Unexport trust commands
This patch deprecates exported trust commands and moves the implementation
details to an unexported function.

Commands that are affected include:

- trust.NewTrustCommand

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit bd8e3e4440)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 14:20:43 +02:00
ae3ee9b47b Unexport plugin commands
This patch deprecates exported plugin commands and moves the implementation
details to an unexported function.

Commands that are affected include:

- plugin.NewPluginCommand

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit c6b7268932)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 14:20:32 +02:00
733762af0a Unexport swarm commands
This patch deprecates exported swarm commands and moves the implementation
details to an unexported function.

Commands that are affected include:

- swarm.NewSwarmCommand

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit bf39340294)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 14:20:16 +02:00
46862878f7 Unexport registry commands
This patch deprecates exported registry commands and moves the implementation
details to an unexported function.

Commands that are affected include:

- registry.NewLoginCommand
- registry.NewLogoutCommand
- registry.NewSearchCommand

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit d4588c711c)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 13:45:11 +02:00
4a2b024c57 Unexport stack commands
This patch deprecates exported stack commands and moves the implementation
details to an unexported function.

Commands that are affected include:

- stack.NewStackCommand

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit 630fe430ff)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 13:41:28 +02:00
82de0590b6 Unexport context command
This patch deprecates exported context commands and moves the implementation
details to an unexported function.

Commands that are affected include:

- context.NewContextCommand

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit 3b0edc794c)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 13:41:25 +02:00
3e36fa624a Unexport volume commands
This patch deprecates exported volume commands and moves the implementation
details to an unexported function.

Commands that are affected include:

- volume.NewVolumeCommand
- volume.NewPruneCommand

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit 9961e39d40)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 13:19:17 +02:00
01ea3a7da7 Unexport service commands
This patch deprecates exported service commands and moves the implementation
details to an unexported function.

Commands that are affected include:

- service.NewServiceCommand

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit 88178eda32)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 12:58:33 +02:00
0f2721ee4f Unexport secret commands
This patch deprecates exported secret commands and moves the implementation
details to an unexported function.

Commands that are affected include:

- secrets.NewSecretCommand

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit e00762ed7d)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 12:55:01 +02:00
6fc62cf5b6 Unexport manifest command
This patch deprecates exported manifest commands and moves the implementation
details to an unexported function.

Commands that are affected include:

- manifest.NewManifestCommand

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit 02fda07211)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 12:54:49 +02:00
c580c9741e Unexport node commands
This patch deprecates exported node commands and moves the implementation
details to an unexported function.

Commands that are affected include:

- node.NewNodeCommand

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit ab3fcf9f9b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 12:54:38 +02:00
ba8b22e783 Unexport network commands
This patch deprecates exported network commands and moves the
implementation details to an unexported function.

Commands that are affected include:

- network.NewNetworkCommand
- network.NewPruneCommand

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit 78a8856c14)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 12:54:20 +02:00
def5bfc11c Unexport system commands
This patch deprecates exported system commands and moves the
implementation details to an unexported function.

Commands that are affected include:

- system.NewVersionCommand
- system.NewInfoCommand
- system.NewSystemCommand
- system.NewEventsCommand
- system.NewInspectCommand

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit cfb8cb91f2)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 12:37:17 +02:00
9d258edf27 Unexport image commands
This patch deprecates exported image commands and moves the
implementation details to an unexported function.

Commands that are affected include:

- image.NewBuildCommand
- image.NewPullCommand
- image.NewPushCommand
- image.NewImagesCommand
- image.NewImageCommand
- image.NewHistoryCommand
- image.NewImportCommand
- image.NewLoadCommand
- image.NewRemoveCommand
- image.NewSaveCommand
- image.NewTagCommand
- image.NewPruneCommand

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit e66a1456d3)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 12:37:17 +02:00
f68a9a06fe Unexport container commands
This patch deprecates exported container commands and moves the
implementation details to an unexported function.

Commands that are affected include:
- container.NewRunCommand
- container.NewExecCommand
- container.NewPsCommand
- container.NewContainerCommand
- container.NewAttachCommand
- container.NewCommitCommand
- container.NewCopyCommand
- container.NewCreateCommand
- container.NewDiffCommand
- container.NewExportCommand
- container.NewKillCommand
- container.NewLogsCommand
- container.NewPauseCommand
- container.NewPortCommand
- container.NewRenameCommand
- container.NewRestartCommand
- container.NewRmCommand
- container.NewStartCommand
- container.NewStatsCommand
- container.NewStopCommand
- container.NewTopCommand
- container.NewUnpauseCommand
- container.NewUpdateCommand
- container.NewWaitCommand
- container.NewPruneCommand

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit 38595fecb6)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 12:37:17 +02:00
60caaa39e0 Unexport config command
This patch unexports the `config` command.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit cce29da061)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 12:37:17 +02:00
c94245da66 Unexport checkpoint command
This patch unexports the `checkpoint` command.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit 3265cead1d)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 12:37:16 +02:00
79c03dcaaf Unexport the builder command and bake stub command
This patch unexports the `builder` and `bake` stub command and it adds
deprecation notices on the exported functions.

It also registers the commands using the new `cli/internal/commands`
package when the init function executes.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit 1b9d0762a5)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-20 12:37:16 +02:00
acb019a344 Merge pull request #6311 from thaJeztah/28.x_backport_deprecate_platform_flags
[28.x backport] cli/command: remove `AddTrustSigningFlags`, `AddTrustVerificationFlags`, `AddPlatformFlag` utilities
2025-08-20 12:04:45 +02:00
5a80c3e98d Merge pull request #6315 from thaJeztah/28.x_backport_auth_cleanups
[28.x backport] cli/command: fix godoc links and inline resolveAuthConfigFromImage
2025-08-20 11:58:27 +02:00
e250694a35 Merge pull request #6313 from thaJeztah/28.x_backport_internalize_registryclient
[28.x backport] cli/registry/client: deprecate and move internal
2025-08-20 11:58:05 +02:00
942eade165 cli/command: inline resolveAuthConfigFromImage
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 4286883b95)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-19 23:42:44 +02:00
a746a99d97 cli/command: fix godoc links
- Use versioned links to github.com/docker/docker packages
- Fix links to RFC 4648, section 5

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 2d3b0b33b4)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-19 23:42:44 +02:00
66c3dbdfed cli/registry/client: deprecate and move internal
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 13010ba673)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-19 22:37:33 +02:00
cf44e3164b Merge pull request #6310 from thaJeztah/28.x_backport_rm_decodeauthconfig
[28.x backport] cli/command: TestRetrieveAuthTokenFromImage: don't decode authconfig
2025-08-19 11:30:51 -07:00
ce3196c726 Merge pull request #6309 from thaJeztah/28.x_backport_docs
[28.x backport] docs/deprecated: legacy links env vars
2025-08-19 11:19:41 -07:00
80884714de cli/command: remove AddPlatformFlag utility
It was only used internally and has no external users. It should not be
used for new uses, because it also adds a minimum API version constraint
and a default from env-var, which must be evaluated for each individual
use of such flags.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 7026e68a71)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-19 18:35:03 +02:00
4e00c31c71 cli/command: remove AddTrustVerificationFlags
It was only used internally; inline it where used.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit c0fbbe05ca)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-19 18:35:03 +02:00
7126bf7d22 [28.x] remove some uses of AddTrustVerificationFlags
These were already removed in master, so adding an extra commit
in the 28.x branch to remove their use.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-19 18:34:56 +02:00
53ed958805 cli/command: remove AddTrustSigningFlags
it was only used internally in a single location, so inline the
code where it's used.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 8c22927978)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-19 18:17:52 +02:00
be307b2925 cli/command: TestRetrieveAuthTokenFromImage: don't decode authconfig
Rewrite the test to not depend on registry.DecodeAuthConfig, which
may be moved internal to the daemon as part of the modules transition.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit ae1727c41e)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-19 18:12:39 +02:00
8f8d39c35e docs: deprecated: fix formatting of deprecated/removed in
- Use sentence-case to follow our docs guidelines.
- Add newlines to prevent these being rendered on a
  single line.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 1d571d178d)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-19 18:09:18 +02:00
c61c0cdb3c docs/deprecated: legacy links env vars
Signed-off-by: Albin Kerouanton <albinker@gmail.com>
Co-authored-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 5c76f7f2d8)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-19 18:05:08 +02:00
830b5f1d2f Merge pull request #6308 from thaJeztah/28.x_backport_mountrecursive
[28.x] docs: add deprecated `bind-nonrecursive` option for `--mount`
2025-08-19 08:06:35 -07:00
abfc1f4c76 Merge pull request #6307 from thaJeztah/28.x_bump_gha
[28.x backport] build(deps): bump actions/checkout from 4 to 5
2025-08-19 08:03:23 -07:00
31fa40cb25 docs: add deprecated bind-nonrecursive option for --mount
The `bind-nonrecursive` option was replaced with the [`bind-recursive`]
option (see [cli-4316], [cli-4671]), but the deprecated docs was not
updated to mention.

Based on abfe4d4629 in master

[`bind-recursive`]: https://docs.docker.com/engine/storage/bind-mounts/#recursive-mounts
[cli-4316]: https://github.com/docker/cli/pull/4316
[cli-4671]: https://github.com/docker/cli/pull/4671

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit abfe4d4629)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-19 16:49:51 +02:00
4881921784 build(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit f2af519f2e)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-19 16:32:19 +02:00
4313a2bbb7 Merge pull request #6294 from thaJeztah/28.x_backport_progress_test
[28.x] cli/command/container: TestRunPullTermination: rewrite with streamformatter
2025-08-19 13:04:32 +02:00
20c34ef13b Merge pull request #6292 from thaJeztah/28.x_backport_deprecate_opts
[28.x backport] opts: deprecate NewNamedListOptsRef, NewNamedMapOpts
2025-08-19 12:50:57 +02:00
34a90bdd81 Merge pull request #6298 from thaJeztah/28.x_backport_plugin_manager_more_deprecations
[28.x backport] cli-plugins/manager: deprecate annotation metadata aliases
2025-08-19 12:50:08 +02:00
98b82d5156 cli-plugins/manager: deprecate annotation metadata aliases
These aliases were added in 292713c887
(part of v28.0), but did not deprecate them. They are no longer used
in the CLI itself, but may be used by cli-plugin implementations.

This deprecates the aliases in `cli-plugins/manager` in favor of
their equivalent in `cli-plugins/manager/metadata`:

- `CommandAnnotationPlugin`
- `CommandAnnotationPluginVendor`
- `CommandAnnotationPluginVersion`
- `CommandAnnotationPluginInvalid`
- `CommandAnnotationPluginCommandPath`

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 72f76f2720)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-18 21:54:05 +02:00
0e474e38f1 [28.x] cli/command/container: TestRunPullTermination: rewrite with streamformatter
This makes the test slightly closer to the actual code in the daemon producing
the progress response;
cd844fd0b2/daemon/images/image_pull.go (L58-L70)
cd844fd0b2/daemon/internal/distribution/utils/progress.go (L14-L34)

This is a modified version of 69854c4e08
with some changes specific to the 28.x branch (the variant on master
had some patches for the moby/api and moby/client transition).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 69854c4e08)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-18 19:17:14 +02:00
6adde56036 Merge pull request #6291 from thaJeztah/28.x_backport_deprecate_quoted_values
[28.x backport] Deprecate special handling for quoted values for TLS flags
2025-08-18 18:35:07 +02:00
7ccd6d29c6 opts: deprecate NewNamedListOptsRef, NewNamedMapOpts
The `NewNamedListOptsRef`, `NewNamedMapOpts` and related `NamedListOpts`,
`NamedMapOpts`, and `NamedOption` interface were added in [moby@677a6b3],
which added support for a `daemon.json` configuration file. That change
required a way to correlate command-line flags with their corresponding
fields in the `daemon.json` to detect conflicting options. At the time,
the CLI and daemon were produced from the same code, and shared packages
for command-line options, but when the CLI was moved to a separate
repository, these options were inherited.

[moby@677a6b3]: 677a6b3506

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 6f0c66c152)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-18 18:28:48 +02:00
14f297204f Merge pull request #6289 from thaJeztah/28.x_backport_hosts_regular_stringarray
[28.x backport] cli/flags: add "hostVar" to handle --host / -H as a single string
2025-08-18 18:28:25 +02:00
d1617cb0c0 Deprecate special handling for quoted values for TLS flags
The `--tlscacert`, `--tlscert`, and `--tlskey` command-line flags had
non-standard behavior for handling values contained in quotes (`"` or `'`).
Normally, quotes are handled by the shell, for example, in the following
example, the shell takes care of handling quotes before passing the values
to the `docker` CLI:

    docker --some-option "some-value-in-quotes" ...

However, when passing values using an equal sign (`=`), this may not happen
and values may be handled including quotes;

    docker --some-option="some-value-in-quotes" ...

This caused issues with "Docker Machine", which used this format as part
of its `docker-machine config` output, and the CLI carried special, non-standard
handling for these flags.

Docker Machine reached EOL, and this special handling made the processing
of flag values inconsistent with other flags used, so this behavior is
deprecated. Users depending on this behavior are recommended to specify
the quoted values using a space between the flag and its value, as illustrated
above.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit ee05a71513)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-18 18:18:04 +02:00
7091e8bea4 cli/flags: add "hostVar" to handle --host / -H as a single string
hostVar is used for the '--host' / '-H' flag to set [ClientOptions.Hosts].
The [ClientOptions.Hosts] field is a slice because it was originally shared
with the daemon config. However, the CLI only allows for a single host to
be specified.

hostVar presents itself as a "string", but stores the value in a string
slice. It produces an error when trying to set multiple values, matching
the check in [getServerHost].

[getServerHost]: 7eab668982/cli/command/cli.go (L542-L551)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit f14eeeb361)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-18 17:12:54 +02:00
ff42ff9f06 cli/flags: use a regular StringArray for the --host / -H flag
The ClientOptions struct and related flags were inherited from the Moby
repository, where originally the CLI and Daemon used the same implementation
and had a "Common" options struct. When the CLI moved to a separate repository,
those structs were duplicated, but some daemon-specific logic remained. For
example, the daemon can be configured to listen on multiple ports and sockets
([moby@dede158]), but the CLI [can only connect to a single host][1]. The
daemon config also had to account for flags conflicting with `daemon.json`,
and use special flag-vars for this ([moby@677a6b3]).

Unfortunately, the `ClientConfig` struct became part of the public API and
is used as argument in various places, but we can remove the use of the
special flag var. This patch replaces the use of `NewNamedListOptsRef`
for a regular `StringArray`.

Unfortunately this changes the flag's type description from `list` to
`stringArray`, but we can look at changing that separately.

[moby@dede158]: dede1585ee
[1]: 0af135e906/docker/docker.go (L191-L193)
[moby@677a6b3]: 677a6b3506

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 5ee2906e78)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-18 17:11:54 +02:00
bcc479b4c3 Merge pull request #6276 from thaJeztah/28.x_backport_cli_internalize_utils
[28.x backport] cli: deprecate VisitAll, DisableFlagsInUseLine utilities, remove HasCompletionArg
2025-08-18 11:41:10 +02:00
6a596c007c Merge pull request #6269 from thaJeztah/28.x_backport_plugin_manager_unexport
[28.x backport] cli-plugins/manager: various fixes and deprecations
2025-08-18 11:24:41 +02:00
79e0b1d7d9 cli-plugins/manager: remove deprecated ResourceAttributesEnvvar
This const was deprecated in 9dc175d6ef,
which is part of v28.0, so let's remove it.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 513ceeec0a)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-17 19:44:54 +02:00
099820d8ca cli-plugins/manager: deprecate metadata aliases
These aliases were added in 4321293972
(part of v28.0), but did not deprecate them. They are no longer used
in the CLI itself, but may be used by cli-plugin implementations.

This deprecates the aliases in `cli-plugins/manager` in favor of
their equivalent in `cli-plugins/manager/metadata`:

- `NamePrefix`
- `MetadataSubcommandName`
- `HookSubcommandName`
- `Metadata`
- `ReexecEnvvar`

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 5876b2941c)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-17 19:44:53 +02:00
ac88b74462 cli-plugins/manager: wrapAsPluginError: don't special-case nil
This was a pattern inheritted from pkg/errors.Wrapf, which ignored
nil errors for convenience. However, it is error-prone, as it is
not obvious when returning a nil-error.

All call-sites using `wrapAsPluginError` already do a check for
nil errors, so remove this code to prevent hard to find bugs.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 50963accec)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-17 19:44:53 +02:00
7f699066bd cli-plugins/manager: pluginError: remove Causer interface
We no longer depend on this interface and it implements Unwrap for
native handling by go stdlib.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit d789bac04a)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-17 19:44:53 +02:00
fe1d790d8e cli-plugins/manager: deprecate "IsNotFound"
These errors satisfy errdefs.IsNotFound, so make it a wrapper, and
deprecate it.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 71460215d3)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-17 19:44:53 +02:00
5e29918a44 cli-plugins/manager: un-export "NewPluginError"
It is for internal use, and no longer needed for testing, now that
the `Plugin` type handles marshalling errors.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 1cc698c68f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-17 19:44:53 +02:00
09efe3f408 cli-plugins/manager: fix Plugin marshaling with regular errors
Go does not by default marshal `error` type fields to JSON. The manager
package therefore implemented a `pluginError` type that implements
[encoding.TextMarshaler]. However, the field was marked as a regular
`error`, which made it brittle; assining any other type of error would
result in the error being discarded in the marshaled JSON (as used in
`docker info` output), resulting in the error being marshaled as `{}`.

This patch adds a custom `MarshalJSON()` on the `Plugin` type itself
so that any error is rendered. It checks if the error used already
implements [encoding.TextMarshaler], otherwise wraps the error in
a `pluginError`.

[encoding.TextMarshaler]: https://pkg.go.dev/encoding#TextMarshaler

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 549d39a89f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-17 19:44:52 +02:00
79dd3a3c79 cli-plugins/manager: un-export "Candidate" interface
It is for internal use for mocking purposes, and is not part
of any public interface / signature.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 54367b3283)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-17 19:44:49 +02:00
a8fe4aaa7f Merge pull request #6272 from thaJeztah/28.x_backport_errdefs_unalias
[28.x backport] remove aliases for containerd/errdefs, disallow docker/errdefs
2025-08-16 15:34:43 -07:00
034dc932d7 remove aliases for containerd/errdefs, disallow docker/errdefs
We transitioned most functionality of docker/errdefs to containerd
errdefs module, and the docker/errdefs package should no longer be
used.

Because of that, there will no longer be ambiguity, so we can remove
the aliases for this package, and use it as "errdefs".

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 89d8c8a2a7)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-16 18:29:23 +02:00
0718529a7e Merge pull request #6221 from thaJeztah/28.x_fork_registry
[28.x] add internal fork of docker/docker/registry
2025-08-16 08:04:59 -07:00
c754ac3c5b Merge pull request #6280 from thaJeztah/28.x_backport_deprecate_ValidateHost
[28.x backport] opts: deprecate ValidateHost utility
2025-08-16 07:21:56 -07:00
5d327e8032 Merge pull request #6277 from thaJeztah/28.x_backport_fix_prune_example
[28.x backport] docs: fix output example for docker system prune
2025-08-16 07:20:24 -07:00
bce66f6126 Merge pull request #6275 from thaJeztah/28.x_backport_deprecate_quotedstring
[28.x backport] opts: deprecate QuotedString
2025-08-15 13:20:06 -07:00
511821052a opts: deprecate ValidateHost utility
The `ValidateHost` option was introduced in [moby@1ba1138] to be used
as validation func for the `--host` flag on the daemon in and CLI in
[moby@5e3f6e7], but is no longer used since [cli@6f61cf0]. which added
support for `ssh://` connections, and required validation elsewhere.

[moby@1ba1138]: 1ba11384bf
[moby@5e3f6e7]: 5e3f6e7023
[cli@6f61cf0]: 6f61cf053a

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit d0ac0acff0)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 22:18:29 +02:00
9fc26d43ed Merge pull request #6278 from thaJeztah/28.x_backport_update-golang-1.24.6
[28.x backport] update to go1.24.6
2025-08-15 13:06:03 -07:00
2b4e0a0f45 update to go1.24.6
- https://github.com/golang/go/issues?q=milestone%3AGo1.24.6+label%3ACherryPickApproved
- full diff: golang/go@go1.24.5...go1.24.6

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

- os/exec: LookPath may return unexpected paths

If the PATH environment variable contains paths which are executables (rather
than just directories), passing certain strings to LookPath ("", ".", and ".."),
can result in the binaries listed in the PATH being unexpectedly returned.

Thanks to Olivier Mengué for reporting this issue.

This is CVE-2025-47906 and Go issue https://go.dev/issue/74466.

- database/sql: incorrect results returned from Rows.Scan

Cancelling a query (e.g. by cancelling the context passed to one of the query
methods) during a call to the Scan method of the returned Rows can result in
unexpected results if other queries are being made in parallel. This can result
in a race condition that may overwrite the expected results with those of
another query, causing the call to Scan to return either unexpected results
from the other query or an error.

We believe this affects most database/sql drivers.

Thanks to Spike Curtis from Coder for reporting this issue.

This is CVE-2025-47907 and https://go.dev/issue/74831.

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

Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
(cherry picked from commit 6769f62746)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 19:16:22 +02:00
5d6e64c4a6 docs: fix output example for docker system prune
The example shows that the `--volumes` option is used, which in current
versions of docker only removes "anonymous" volumes, but preserves named
volume:

    $ docker system prune -a --volumes
    ...
            - all anonymous volumes not used by at least one container
    ...

But the example output showed that a named volume ("named-vol") was
deleted;

    Deleted Volumes:
    named-vol

Co-authored-by: Roberto Villarreal <rrjjvv@yahoo.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit bf13010df8)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 19:13:57 +02:00
ed52ada4a3 cmd/docker: fix some minor linting issues
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 5a38118956)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 19:11:17 +02:00
60d16e20ac cli: deprecate VisitAll, DisableFlagsInUseLine utilities
These utilities were only used internally; create a local copy
where used, and deprecate the ones in cli.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 6bd8a4b2b5)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 19:11:17 +02:00
713ed839fe cli: remove HasCompletionArg utility
It was only used in a single place and has no external consumers.
Move it to where it's used to keep things together.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 5a99022556)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 19:11:17 +02:00
6bfee62d6d opts: deprecate QuotedString
The `QuotedString` option was added in [moby@e4c1f07] and [moby@abe32de]
to work around a regression in Docker 1.13 that caused `docker-machine`
to fail. `docker-machine` produced instructions on how to set up a cli
to connect to the Machine it produced. These instructions used quotes
around the paths for TLS certificates, but with an `=` for the flag's
values instead of a space; due to this the shell would not handle
stripping quotes, so the CLI would now get the value including quotes.

Preserving quotes in such cases is expected (and standard behavior), but
versions of Docker before 1.13 used a custom "mflag" package for flag
parsing, and that package contained custom handling for quotes (added
in [moby@0e9c40e]).

For other flags, this problem could be solved by the user, but as these
instructions were produced by `docker-machine`'s `config` command, an
exception was made for the `--tls-xxx` flags. From [moby-29761]:

> The flag trimming behaviour is really unusual, and I would say unexpected.
> I think removing it is generally the right idea. Since we have one very
> common case where it's necessary for backwards compatibility we need to
> add a special case, but I don't think we should apply that case to every
> flag.

The `QuotedString` implementation has various limitations, as it doesn't
follow the same handling of quotes as a shell would.

Given that Docker Machine reached EOL a long time ago and other options,
such as `docker context`, have been added to configure the CLI to connect
to a specific host (with corresponding TLS configuration), we should remove
the special handling for these flags, as it's inconsitent with all other
flags, and not worth maintaining for a tool that no longer exists.

This patch deprecates the `QuotedString` option and removes its use. A
temporary, non-exported copy is added, but will be removed in the next
release.

[moby-29761]: https://github.com/moby/moby/issues/29761#issuecomment-270211265
[moby@e4c1f07]: e4c1f07729
[moby@abe32de]: abe32de6b4
[moby@0e9c40e]: 0e9c40eb82
[moby@c79a169]: c79a169a35

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 187a942a88)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 19:07:52 +02:00
efdf008933 internal/registry: remove RepositoryInfo, add NewIndexInfo
Most places only use IndexInfo (and may not even need that), so replace
the use of ParseRepositoryInfo for NewIndexInfo, and move the RepositoryInfo
type to the trust package, which uses it as part of its ImageRefAndAuth
struct.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 21e8bbc8a2)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 15:45:49 +02:00
28ffe2416d internal/registry: ParseRepositoryInfo: remove unused error return
Removed the error return from the `ParseRepositoryInfo` function.
There are no validation steps inside `ParseRepositoryInfo` which
could cause an error, so we always returned a nil error.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 86b5b528a6)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 15:45:49 +02:00
b0bb4acc11 internal/registry: fix linting issues (revive)
internal/registry/errors.go:26:43: use-any: since Go 1.18 'interface{}' can be replaced by 'any' (revive)
    func invalidParamf(format string, args ...interface{}) error {
                                              ^
    internal/registry/registry_mock_test.go:52:51: use-any: since Go 1.18 'interface{}' can be replaced by 'any' (revive)
    func writeResponse(w http.ResponseWriter, message interface{}, code int) {
                                                      ^

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit f907c7a4b0)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 15:45:49 +02:00
2e5a36728b cli/command/system: remove use of Mirrors field in test
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit cd277a5815)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 15:45:49 +02:00
a5a17eb2c7 internal/registry: remove pkg/errors
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit c297770d2d)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 15:45:48 +02:00
e1f79927f3 internal/registry: define local serviceConfig
The registry.ServiceConfig struct in the API types was meant for the
registry configuration on the daemon side; it has variuos fields we
don't use, defines methods for (un)marshaling JSON, and a custom version
of `net.IPNet`, also to (un)marshal JSON.

None of that is needed, so let's change it to a local type, and implement
a constructor (as we now only have "insecure registries" to care
about).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 219cfc8b7d)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 15:45:48 +02:00
cab6012db9 internal/registry: remove ValidateIndexName
It was written to be used as validate-func for command-line flags, which
we don't use it for (which for CLI-flags includes normalizing the value).

The validation itself didn't add much; it only checked the registry didn't
start or end with a hyphen (which would still fail when parsing).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 2607ba8062)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 15:45:48 +02:00
75a4cbbf8e internal/registry: remove duplicate endpoint methods
now that we no longer need to account for mirrors, these were
identical, so just use a single one.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 5322affc9f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 15:45:48 +02:00
9071d3868c internal/registry: remove NewStaticCredentialStore
It was only used in a single place; inline it there.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit dc41365b56)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 15:45:48 +02:00
e7f4f04156 internal/registry: remove PingResponseError
It's not matched anywhere, so we can just return a plain error.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit dad2e67860)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 15:45:48 +02:00
7e01a3a8a9 internal/registry: Service.Auth remove unused statusmessage return
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 7cf245d2f7)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 15:45:47 +02:00
72f85ccd16 internal/registry: remove code related to mirrors
The CLI does not have information about mirrors, and doesn't
configure them, so we can remove these parts.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit e0b351b3d9)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 15:45:47 +02:00
ecd54bc6dd internal/registry: remove dead code
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 7716219e17)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 15:45:47 +02:00
8b9baffdf7 add internal fork of docker/docker/registry
This adds an internal fork of [github.com/docker/docker/registry], taken
at commit [moby@f651a5d]. Git history  was not preserved in this fork,
but can be found using the URLs provided.

This fork was created to remove the dependency on the "Moby" codebase,
and because the CLI only needs a subset of its features. The original
package was written specifically for use in the daemon code, and includes
functionality that cannot be used in the CLI.

[github.com/docker/docker/registry]: https://pkg.go.dev/github.com/docker/docker@v28.3.2+incompatible/registry
[moby@49306c6]: 49306c607b/registry

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit f6b90bc253)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 15:45:47 +02:00
93a51c39f4 cli/command/registry: remove uses of registry.ParseSearchIndexInfo
This utility was only used in the CLI, but the implementation was
based on it being used on the daemon side, so included resolving
the host's IP-address, mirrors, etc.

The only reason it's used in the CLI is to provide credentials for
the registry that's being searched, so reduce it to just that.

There's more cleaning up to do in this area, so to make our lives
easier, it's implemented locally as non-exported functions; likely
to be replaced with something else.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit e504faf6da)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-15 15:45:45 +02:00
2dc9daae32 Merge pull request #6265 from thaJeztah/28.x_backport_fork_remotecontext
[28.x backport] add local fork of github.com/docker/docker/builder/remotecontext
2025-08-15 05:51:05 -07:00
a4f8f22a33 Merge pull request #6190 from thaJeztah/fork_remotecontext
add local fork of github.com/docker/docker/builder/remotecontext

(cherry picked from commit 8c317ad3fd)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-14 21:50:07 +02:00
7d3bde083c cli/command/image: move build-context detection to build
Removes direct imports of github.com/docker/docker/builder in
the image package, to be moved later.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 260f1dbebb)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-14 21:50:07 +02:00
32207833d0 Merge pull request #6262 from thaJeztah/28.x_backport_no_pkg_process
[28.x backport] cli/connhelper: remove dependency on pkg/process
2025-08-14 21:49:42 +02:00
7399781944 cli/connhelper: remove dependency on pkg/process
This package will not be included in the api or client modules, and
we're currently only using a single function of it, and only the
unix implementation, so let's fork it for now (although the package
may be moved to moby/sys).

This removes the last dependency on github.com/docker/docker.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 2abcbf842f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-14 18:45:43 +02:00
efee5879a1 Merge pull request #6264 from thaJeztah/28.x_merge_28.3.3
[28.x] merge v28.3.3 tag into v28.x
2025-08-14 18:42:32 +02:00
b82e19efe0 [28.x] merge v28.3.3 tag into v28.x
The v28.3.3 tag was created from master, but the v28.x branch
wasn't fast-forwarded, and PR's merged after that. This should
bring the v28.3.3 tag's changes into the v28.x branch.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-14 17:45:14 +02:00
eceff3dbc5 Merge pull request #6256 from thaJeztah/28.x_backport_remove_prompt_privilege_func
[28.x backport] cli/command: remove interactive login prompt from docker push/pull, deprecate RegistryAuthenticationPrivilegedFunc
2025-08-14 14:43:15 +02:00
3598fc3745 Merge pull request #6259 from thaJeztah/28.x_backport_rm_completion
[28.x backport] cli/command/completion: remove deprecated ValidArgsFn
2025-08-14 10:51:34 +02:00
2df466710b Merge pull request #6258 from thaJeztah/28.x_backport_remove_RepoNameForReference
[28.x backport] cli/registry/client: remove deprecated RepoNameForReference
2025-08-14 10:22:59 +02:00
29f2ce760a Merge pull request #6257 from thaJeztah/28.x_backport_remove_deprecated
[28.x backport] cli/command: remove deprecated CopyToFile, ConfigureAuth utilities
2025-08-14 10:22:07 +02:00
363f4c0031 cli/command/completion: remove deprecated ValidArgsFn
This was deprecated in 9f19820f88, which
is part of v28.x, and unlikely used externally.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 5052a39915)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-13 18:06:23 +02:00
6d4ffec3fb cli/registry/client: remove deprecated RepoNameForReference
This was deprecated in 6f46cd2f4b,
which is part of v28.x, and no longer used, so we can remove it.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit a87bde0068)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-13 18:02:48 +02:00
5c1cee4630 cli/command: remove deprecated ConfigureAuth utility
It was deprecated in 6e4818e7d6, which
is part of v28.x and backported to v27.x.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 22cc0e90ae)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-13 17:54:06 +02:00
88274f4805 cli/command: remove deprecated CopyToFile utility
It was deprecated in 7cc6b8ebf4, which is
part of v28.x

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit de54347518)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-13 17:53:58 +02:00
5566c3a9b8 cli/command: remove usages of RegistryAuthenticationPrivilegedFunc
This patch deprecates the unused `RegistryAuthenticationPrivilegedFunc`.
The function would prompt the user when the registry returns a 403 after trying
the initial auth value set in `RegistryAuth`.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit 29263e865b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-13 17:38:45 +02:00
5edc6748f4 cli/command: remove interactive login prompt from docker push/pull
This patch removes the interactive prompts from `docker push/pull`.
The prompt would only execute on a response status code 403 from the registry
after trying the value set in `RegistryAuth`. Docker Hub could return 404
instead or 429, which would never execute the prompt.

The UX regarding the prompt is also questionable since the user might
not actually want to authenticate with a registry and the CLI could fail fast
instead. The user can always run `docker login` or set the `DOCKER_AUTH_CONFIG`
environment variable to get authenticated.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
(cherry picked from commit 2b56b66b10)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-13 17:38:29 +02:00
6007e83a75 Merge pull request #6218 from thaJeztah/28.x_bump_deps
[28.x] vendor: github.com/docker/docker v28.3.3, github.com/opencontainers/image-spec v1.1.1
2025-07-29 20:17:56 +02:00
ce2a0a4ecb vendor: github.com/opencontainers/image-spec v1.1.1
full diff: https://github.com/opencontainers/image-spec/compare/v1.1.0...v1.1.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-29 18:10:03 +02:00
3cd108fcd0 vendor: github.com/docker/docker v28.3.3
full diff: https://github.com/docker/docker/compare/v28.3.1...v28.3.3

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-29 18:09:24 +02:00
1641 changed files with 88449 additions and 53157 deletions

View File

@ -35,7 +35,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
-
name: Create matrix
id: platforms
@ -88,7 +88,7 @@ jobs:
fi
-
name: Upload artifacts
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: ${{ env.ARTIFACT_NAME }}
path: /tmp/out/*
@ -143,7 +143,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
-
name: Create matrix
id: platforms

View File

@ -46,7 +46,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
fetch-depth: 2
# CodeQL 2.16.4's auto-build added support for multi-module repositories,
@ -61,19 +61,19 @@ jobs:
ln -s vendor.sum go.sum
-
name: Update Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version: "1.25.4"
go-version: "1.24.7"
-
name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@v3
with:
languages: go
-
name: Autobuild
uses: github/codeql-action/autobuild@v4
uses: github/codeql-action/autobuild@v3
-
name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@v3
with:
category: "/language:go"

View File

@ -37,14 +37,14 @@ jobs:
- alpine
- debian
engine-version:
- rc # latest rc
- 29 # latest
- 28 # latest - 1
- 25 # mirantis lts
- 28 # latest
- 27 # latest - 1
- 26 # github actions default
- 23 # mirantis lts
steps:
-
name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
-
name: Update daemon.json
run: |

View File

@ -53,25 +53,24 @@ jobs:
fail-fast: false
matrix:
os:
- macos-14 # macOS 14 on arm64 (Apple Silicon M1)
- macos-15-intel # macOS 15 on Intel
- macos-15 # macOS 15 on arm64 (Apple Silicon M1)
- macos-13 # macOS 13 on Intel
- macos-14 # macOS 14 on arm64 (Apple Silicon M1)
# - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history
steps:
-
name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
path: ${{ env.GOPATH }}/src/github.com/docker/cli
-
name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version: "1.25.4"
go-version: "1.24.7"
-
name: Test
run: |
go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust')
go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/')
go tool cover -func=/tmp/coverage.txt
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
shell: bash

View File

@ -11,23 +11,18 @@ permissions:
on:
pull_request:
types: [opened, edited, labeled, unlabeled, synchronize]
types: [opened, edited, labeled, unlabeled]
jobs:
check-labels:
check-area-label:
runs-on: ubuntu-24.04
timeout-minutes: 120 # guardrails timeout for the whole job
steps:
- name: Missing `area/` label
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
run: |
echo "::error::Every PR with an 'impact/*' label should also have an 'area/*' label"
exit 1
- name: Missing `kind/` label
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'kind/')
run: |
echo "::error::Every PR with an 'impact/*' label should also have a 'kind/*' label"
exit 1
- name: OK
run: exit 0

View File

@ -48,7 +48,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
-
name: Generate
shell: 'script --return --quiet --command "bash {0}"'
@ -74,7 +74,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
-
name: Run
shell: 'script --return --quiet --command "bash {0}"'

View File

@ -5,7 +5,7 @@ run:
# which causes it to fallback to go1.17 semantics.
#
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
go: "1.25.4"
go: "1.24.7"
timeout: 5m
@ -154,7 +154,6 @@ linters:
arguments: [200]
- name: unused-receiver # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver
- name: use-any # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
- name: use-errors-new # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#use-errors-new
usetesting:
os-chdir: false # FIXME(thaJeztah): Disable `os.Chdir()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478

View File

@ -64,14 +64,11 @@ Arko Dasgupta <arko@tetrate.io> <arko.dasgupta@docker.com>
Arko Dasgupta <arko@tetrate.io> <arkodg@users.noreply.github.com>
Arnaud Porterie <icecrime@gmail.com>
Arnaud Porterie <icecrime@gmail.com> <arnaud.porterie@docker.com>
Arthur Flageul <arthur.flageul@gmail.com>
Arthur Flageul <arthur.flageul@gmail.com> <arthur.flageul@docker.com>
Arthur Gautier <baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net>
Arthur Peka <arthur.peka@outlook.com> <arthrp@users.noreply.github.com>
Austin Vazquez <austin.vazquez@docker.com>
Austin Vazquez <austin.vazquez@docker.com> <55906459+austinvazquez@users.noreply.github.com>
Austin Vazquez <austin.vazquez@docker.com> <austin.vazquez.dev@gmail.com>
Austin Vazquez <austin.vazquez@docker.com> <macedonv@amazon.com>
Austin Vazquez <austin.vazquez.dev@gmail.com>
Austin Vazquez <austin.vazquez.dev@gmail.com> <55906459+austinvazquez@users.noreply.github.com>
Austin Vazquez <austin.vazquez.dev@gmail.com> <macedonv@amazon.com>
Avi Miller <avi.miller@oracle.com> <avi.miller@gmail.com>
Ben Bonnefoy <frenchben@docker.com>
Ben Golub <ben.golub@dotcloud.com>
@ -153,8 +150,6 @@ Dave Henderson <dhenderson@gmail.com> <Dave.Henderson@ca.ibm.com>
Dave Tucker <dt@docker.com> <dave@dtucker.co.uk>
David Alvarez <david.alvarez@flyeralarm.com>
David Alvarez <david.alvarez@flyeralarm.com> <busilezas@gmail.com>
David Dooling <david.dooling@docker.com>
David Dooling <david.dooling@docker.com> <dooling@gmail.com>
David Karlsson <david.karlsson@docker.com>
David Karlsson <david.karlsson@docker.com> <35727626+dvdksn@users.noreply.github.com>
David M. Karr <davidmichaelkarr@gmail.com>

12
AUTHORS
View File

@ -63,7 +63,6 @@ Andreas Köhler <andi5.py@gmx.net>
Andres G. Aragoneses <knocte@gmail.com>
Andres Leon Rangel <aleon1220@gmail.com>
Andrew France <andrew@avito.co.uk>
Andrew He <he.andrew.mail@gmail.com>
Andrew Hsu <andrewhsu@docker.com>
Andrew Macpherson <hopscotch23@gmail.com>
Andrew McDonnell <bugs@andrewmcdonnell.net>
@ -87,12 +86,11 @@ Archimedes Trajano <developer@trajano.net>
Arko Dasgupta <arko@tetrate.io>
Arnaud Porterie <icecrime@gmail.com>
Arnaud Rebillout <elboulangero@gmail.com>
Arthur Flageul <arthur.flageul@gmail.com>
Arthur Peka <arthur.peka@outlook.com>
Ashly Mathew <ashly.mathew@sap.com>
Ashwini Oruganti <ashwini.oruganti@gmail.com>
Aslam Ahemad <aslamahemad@gmail.com>
Austin Vazquez <austin.vazquez@docker.com>
Austin Vazquez <austin.vazquez.dev@gmail.com>
Azat Khuyiyakhmetov <shadow_uz@mail.ru>
Bardia Keyoumarsi <bkeyouma@ucsc.edu>
Barnaby Gray <barnaby@pickle.me.uk>
@ -137,12 +135,10 @@ Cao Weiwei <cao.weiwei30@zte.com.cn>
Carlo Mion <mion00@gmail.com>
Carlos Alexandro Becker <caarlos0@gmail.com>
Carlos de Paula <me@carlosedp.com>
carsontham <carsontham@outlook.com>
Carston Schilds <Carston.Schilds@visier.com>
Casey Korver <casey@korver.dev>
Ce Gao <ce.gao@outlook.com>
Cedric Davies <cedricda@microsoft.com>
Cesar Talledo <cesar.talledo@docker.com>
Cezar Sa Espinola <cezarsa@gmail.com>
Chad Faragher <wyckster@hotmail.com>
Chao Wang <wangchao.fnst@cn.fujitsu.com>
@ -224,7 +220,7 @@ David Alvarez <david.alvarez@flyeralarm.com>
David Beitey <david@davidjb.com>
David Calavera <david.calavera@gmail.com>
David Cramer <davcrame@cisco.com>
David Dooling <david.dooling@docker.com>
David Dooling <dooling@gmail.com>
David Gageot <david@gageot.net>
David Karlsson <david.karlsson@docker.com>
David le Blanc <systemmonkey42@users.noreply.github.com>
@ -269,7 +265,6 @@ Eli Uriegas <eli.uriegas@docker.com>
Eli Uriegas <seemethere101@gmail.com>
Elias Faxö <elias.faxo@tre.se>
Elliot Luo <956941328@qq.com>
Eng Zer Jun <engzerjun@gmail.com>
Eric Bode <eric.bode@foundries.io>
Eric Curtin <ericcurtin17@gmail.com>
Eric Engestrom <eric@engestrom.ch>
@ -350,7 +345,6 @@ Henning Sprang <henning.sprang@gmail.com>
Henry N <henrynmail-github@yahoo.de>
Hernan Garcia <hernandanielg@gmail.com>
Hongbin Lu <hongbin034@gmail.com>
Hossein Abbasi <16090309+hsnabszhdn@users.noreply.github.com>
Hu Keping <hukeping@huawei.com>
Huayi Zhang <irachex@gmail.com>
Hugo Chastel <Hugo-C@users.noreply.github.com>
@ -601,7 +595,6 @@ Michael Prokop <github@michael-prokop.at>
Michael Scharf <github@scharf.gr>
Michael Spetsiotis <michael_spets@hotmail.com>
Michael Steinert <mike.steinert@gmail.com>
Michael Tews <michael@tews.dev>
Michael West <mwest@mdsol.com>
Michal Minář <miminar@redhat.com>
Michał Czeraszkiewicz <czerasz@gmail.com>
@ -903,7 +896,6 @@ Wenlong Zhang <zhangwenlong@loongson.cn>
Wenzhi Liang <wenzhi.liang@gmail.com>
Wes Morgan <cap10morgan@gmail.com>
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
Will Wang <willww64@gmail.com>
William Henry <whenry@redhat.com>
Xianglin Gao <xlgao@zju.edu.cn>
Xiaodong Liu <liuxiaodong@loongson.cn>

View File

@ -5,32 +5,26 @@ ARG BASE_VARIANT=alpine
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
# that's also available as alpine image variant for the Golang version used.
ARG ALPINE_VERSION=3.22
ARG ALPINE_VERSION=3.21
ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.25.4
# XX_VERSION specifies the version of the xx utility to use.
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
ARG XX_VERSION=1.7.0
# GOVERSIONINFO_VERSION is the version of GoVersionInfo to install.
# It must be a valid tag from https://github.com/josephspurrier/goversioninfo
ARG GOVERSIONINFO_VERSION=v1.5.0
ARG GO_VERSION=1.24.7
ARG XX_VERSION=1.6.1
ARG GOVERSIONINFO_VERSION=v1.4.1
# GOTESTSUM_VERSION sets the version of gotestsum to install in the dev container.
# It must be a valid tag in the https://github.com/gotestyourself/gotestsum repository.
ARG GOTESTSUM_VERSION=v1.13.0
ARG GOTESTSUM_VERSION=v1.12.3
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
# It must be a tag in the docker.io/docker/buildx-bin image repository
# on Docker Hub.
ARG BUILDX_VERSION=0.29.1
ARG BUILDX_VERSION=0.25.0
# COMPOSE_VERSION is the version of compose to install in the dev container.
# It must be a tag in the docker.io/docker/compose-bin image repository
# on Docker Hub.
ARG COMPOSE_VERSION=v2.40.0
ARG COMPOSE_VERSION=v2.38.2
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
@ -97,7 +91,7 @@ ENV GO111MODULE=auto
RUN --mount=type=bind,target=.,rw \
--mount=type=cache,target=/root/.cache \
--mount=type=cache,target=/go/pkg/mod \
gotestsum -- -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust')
gotestsum -- -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/')
FROM scratch AS test-coverage
COPY --from=test /tmp/coverage.txt /coverage.txt
@ -122,6 +116,10 @@ FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
FROM docker/compose-bin:${COMPOSE_VERSION} AS compose
FROM e2e-base-${BASE_VARIANT} AS e2e
ARG NOTARY_VERSION=v0.6.1
ADD --chmod=0755 https://github.com/theupdateframework/notary/releases/download/${NOTARY_VERSION}/notary-Linux-amd64 /usr/local/bin/notary
COPY --link e2e/testdata/notary/root-ca.cert /usr/share/ca-certificates/notary.cert
RUN echo 'notary.cert' >> /etc/ca-certificates.conf && update-ca-certificates
COPY --link --from=gotestsum /out/gotestsum /usr/bin/gotestsum
COPY --link --from=build /out ./build/
COPY --link --from=build-plugins /out ./build/

View File

@ -34,12 +34,12 @@ test: test-unit ## run tests
.PHONY: test-unit
test-unit: ## run unit tests, to change the output format use: GOTESTSUM_FORMAT=(dots|short|standard-quiet|short-verbose|standard-verbose) make test-unit
gotestsum -- $${TESTDIRS:-$(shell go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust')} $(TESTFLAGS)
gotestsum -- $${TESTDIRS:-$(shell go list ./... | grep -vE '/vendor/|/e2e/')} $(TESTFLAGS)
.PHONY: test-coverage
test-coverage: ## run test coverage
mkdir -p $(CURDIR)/build/coverage
gotestsum -- $(shell go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust') -coverprofile=$(CURDIR)/build/coverage/coverage.txt
gotestsum -- $(shell go list ./... | grep -vE '/vendor/|/e2e/') -coverprofile=$(CURDIR)/build/coverage/coverage.txt
.PHONY: lint
lint: ## run all the lint tools
@ -52,7 +52,7 @@ shellcheck: ## run shellcheck validation
.PHONY: fmt
fmt: ## run gofumpt (if present) or gofmt
@if command -v gofumpt > /dev/null; then \
gofumpt -w -d -lang=1.24 . ; \
gofumpt -w -d -lang=1.23 . ; \
else \
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \
fi
@ -69,15 +69,6 @@ dynbinary: ## build dynamically linked binary
plugins: ## build example CLI plugins
scripts/build/plugins
.PHONY: trust-plugin
trust-plugin: ## build docker-trust CLI plugins
scripts/build/trust-plugin
.PHONY: install-trust-plugin
install-trust-plugin: trust-plugin
install-trust-plugin: ## install docker-trust CLI plugins
install -D -m 0755 "$$(readlink -f build/docker-trust)" /usr/libexec/docker/cli-plugins/docker-trust
.PHONY: vendor
vendor: ## update vendor with go modules
rm -rf vendor

View File

@ -1 +1 @@
29.0.0-dev
28.4.0-dev

View File

@ -8,7 +8,6 @@ import (
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
@ -26,7 +25,7 @@ func main() {
Short: "Print the API version of the server",
RunE: func(_ *cobra.Command, _ []string) error {
apiClient := dockerCLI.Client()
ping, err := apiClient.Ping(context.Background(), client.PingOptions{})
ping, err := apiClient.Ping(context.Background())
if err != nil {
return err
}

View File

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

View File

@ -32,12 +32,14 @@ func (c *fakeCandidate) Metadata() ([]byte, error) {
func TestValidateCandidate(t *testing.T) {
const (
goodPluginName = metadata.NamePrefix + "goodplugin"
builtinName = metadata.NamePrefix + "builtin"
builtinAlias = metadata.NamePrefix + "alias"
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
builtinName = metadata.NamePrefix + "builtin"
builtinAlias = metadata.NamePrefix + "alias"
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
metaExperimental = `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`
)
fakeroot := &cobra.Command{Use: "docker"}
@ -49,103 +51,31 @@ func TestValidateCandidate(t *testing.T) {
})
for _, tc := range []struct {
name string
plugin *fakeCandidate
name string
c *fakeCandidate
// Either err or invalid may be non-empty, but not both (both can be empty for a good plugin).
err string
invalid string
expVer string
}{
// Invalid cases.
{
name: "empty path",
plugin: &fakeCandidate{path: ""},
err: "plugin candidate path cannot be empty",
},
{
name: "bad prefix",
plugin: &fakeCandidate{path: badPrefixPath},
err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix),
},
{
name: "bad path",
plugin: &fakeCandidate{path: badNamePath},
invalid: "did not match",
},
{
name: "builtin command",
plugin: &fakeCandidate{path: builtinName},
invalid: `plugin "builtin" duplicates builtin command`,
},
{
name: "builtin alias",
plugin: &fakeCandidate{path: builtinAlias},
invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`,
},
{
name: "fetch failure",
plugin: &fakeCandidate{path: goodPluginPath, exec: false},
invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath),
},
{
name: "metadata not json",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`},
invalid: "invalid character",
},
{
name: "empty schemaversion",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`},
invalid: `plugin SchemaVersion version cannot be empty`,
},
{
name: "invalid schemaversion",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`},
invalid: `plugin SchemaVersion "xyzzy" has wrong format: must be <major>.<minor>.<patch>`,
},
{
name: "invalid schemaversion major",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "2.0.0"}`},
invalid: `plugin SchemaVersion "2.0.0" is not supported: must be lower than 2.0.0`,
},
{
name: "no vendor",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`},
invalid: "plugin metadata does not define a vendor",
},
{
name: "empty vendor",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`},
invalid: "plugin metadata does not define a vendor",
},
// Valid cases.
{
name: "valid",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`},
expVer: "0.1.0",
},
{
// Including the deprecated "experimental" field should not break processing.
name: "with legacy experimental",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`},
expVer: "0.1.0",
},
{
// note that this may not be supported by older CLIs
name: "new minor schema version",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.2.0", "Vendor": "e2e-testing"}`},
expVer: "0.2.0",
},
{
// note that this may not be supported by older CLIs
name: "new major schema version",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "1.0.0", "Vendor": "e2e-testing"}`},
expVer: "1.0.0",
},
/* Each failing one of the tests */
{name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
{name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix)},
{name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
{name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`},
{name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`},
{name: "fetch failure", c: &fakeCandidate{path: goodPluginPath, exec: false}, invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath)},
{name: "metadata not json", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`}, invalid: "invalid character"},
{name: "empty schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`}, invalid: `plugin SchemaVersion "" is not valid`},
{name: "invalid schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`}, invalid: `plugin SchemaVersion "xyzzy" is not valid`},
{name: "no vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`}, invalid: "plugin metadata does not define a vendor"},
{name: "empty vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`}, invalid: "plugin metadata does not define a vendor"},
// This one should work
{name: "valid", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}},
{name: "experimental + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental}},
} {
t.Run(tc.name, func(t *testing.T) {
p, err := newPlugin(tc.plugin, fakeroot.Commands())
p, err := newPlugin(tc.c, fakeroot.Commands())
switch {
case tc.err != "":
assert.ErrorContains(t, err, tc.err)
@ -156,7 +86,7 @@ func TestValidateCandidate(t *testing.T) {
default:
assert.NilError(t, err)
assert.Equal(t, metadata.NamePrefix+p.Name, goodPluginName)
assert.Equal(t, p.SchemaVersion, tc.expVer)
assert.Equal(t, p.SchemaVersion, "0.1.0")
assert.Equal(t, p.Vendor, "e2e-testing")
}
})

View File

@ -38,7 +38,6 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
rootCmd.AddCommand(&cobra.Command{
Use: p.Name,
Short: p.ShortDescription,
Hidden: p.Hidden,
Run: func(_ *cobra.Command, _ []string) {},
Annotations: annotations,
DisableFlagParsing: true,

View File

@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.23
package manager

View File

@ -2,7 +2,6 @@ package manager
import (
"context"
"errors"
"os"
"os/exec"
"path/filepath"
@ -14,12 +13,21 @@ import (
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/debug"
"github.com/fvbommel/sortorder"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)
const (
// ReexecEnvvar is the name of an ennvar which is set to the command
// used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI.
//
// Deprecated: use [metadata.ReexecEnvvar]. This alias will be removed in the next release.
ReexecEnvvar = metadata.ReexecEnvvar
)
// errPluginNotFound is the error returned when a plugin could not be found.
type errPluginNotFound string
@ -29,6 +37,13 @@ func (e errPluginNotFound) Error() string {
return "Error: No such CLI plugin: " + string(e)
}
// IsNotFound is true if the given error is due to a plugin not being found.
//
// Deprecated: use [errdefs.IsNotFound].
func IsNotFound(err error) bool {
return errdefs.IsNotFound(err)
}
// getPluginDirs returns the platform-specific locations to search for plugins
// in order of preference.
//
@ -59,17 +74,9 @@ func addPluginCandidatesFromDir(res map[string][]string, d string) {
return
}
for _, dentry := range dentries {
switch mode := dentry.Type() & os.ModeType; mode { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list
case os.ModeSymlink:
if !debug.IsEnabled() {
// Skip broken symlinks unless debug is enabled. With debug
// enabled, this will print a warning in "docker info".
if _, err := os.Stat(filepath.Join(d, dentry.Name())); errors.Is(err, os.ErrNotExist) {
continue
}
}
case 0:
// Regular file, keep going
switch dentry.Type() & os.ModeType { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list
case 0, os.ModeSymlink:
// Regular file or symlink, keep going
default:
// Something else, ignore.
continue
@ -179,7 +186,7 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
// have been provided by cobra to our caller. This is because
// they lack e.g. global options which we must propagate here.
args := os.Args[1:]
if !isValidPluginName(name) {
if !pluginNameRe.MatchString(name) {
// We treat this as "not found" so that callers will
// fallback to their "invalid" command path.
return nil, errPluginNotFound(name)

View File

@ -38,7 +38,7 @@ func TestListPluginCandidates(t *testing.T) {
"plugins3-target", // Will be referenced as a symlink from below
fs.WithFile("docker-plugin1", ""),
fs.WithDir("ignored3"),
fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is ignored
fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is still a candidate (but would fail tests later)
fs.WithFile("non-plugin-symlinked", ""), // This shouldn't appear, but ...
fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should.
),
@ -72,6 +72,9 @@ func TestListPluginCandidates(t *testing.T) {
"hardlink2": {
dir.Join("plugins2", "docker-hardlink2"),
},
"brokensymlink": {
dir.Join("plugins3", "docker-brokensymlink"),
},
"symlinked": {
dir.Join("plugins3", "docker-symlinked"),
},

View File

@ -0,0 +1,31 @@
package manager
import (
"github.com/docker/cli/cli-plugins/metadata"
)
const (
// NamePrefix is the prefix required on all plugin binary names
//
// Deprecated: use [metadata.NamePrefix]. This alias will be removed in a future release.
NamePrefix = metadata.NamePrefix
// MetadataSubcommandName is the name of the plugin subcommand
// which must be supported by every plugin and returns the
// plugin metadata.
//
// Deprecated: use [metadata.MetadataSubcommandName]. This alias will be removed in a future release.
MetadataSubcommandName = metadata.MetadataSubcommandName
// HookSubcommandName is the name of the plugin subcommand
// which must be implemented by plugins declaring support
// for hooks in their metadata.
//
// Deprecated: use [metadata.HookSubcommandName]. This alias will be removed in a future release.
HookSubcommandName = metadata.HookSubcommandName
)
// Metadata provided by the plugin.
//
// Deprecated: use [metadata.Metadata]. This alias will be removed in a future release.
type Metadata = metadata.Metadata

View File

@ -9,13 +9,15 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/internal/lazyregexp"
"github.com/spf13/cobra"
)
var pluginNameRe = lazyregexp.New("^[a-z][a-z0-9]*$")
// Plugin represents a potential plugin with all it's metadata.
type Plugin struct {
metadata.Metadata
@ -83,8 +85,8 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
}
// Now apply the candidate tests, so these update p.Err.
if !isValidPluginName(p.Name) {
p.Err = newPluginError("plugin candidate %q did not match %q", p.Name, pluginNameFormat)
if !pluginNameRe.MatchString(p.Name) {
p.Err = newPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String())
return p, nil
}
@ -116,8 +118,8 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
p.Err = wrapAsPluginError(err, "invalid metadata")
return p, nil
}
if err := validateSchemaVersion(p.Metadata.SchemaVersion); err != nil {
p.Err = &pluginError{cause: err}
if p.Metadata.SchemaVersion != "0.1.0" {
p.Err = newPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion)
return p, nil
}
if p.Metadata.Vendor == "" {
@ -127,31 +129,6 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
return p, nil
}
// validateSchemaVersion validates if the plugin's schemaVersion is supported.
//
// The current schema-version is "0.1.0", but we don't want to break compatibility
// until v2.0.0 of the schema version. Check for the major version to be < 2.0.0.
//
// Note that CLI versions before 28.4.1 may not support these versions as they were
// hard-coded to only accept "0.1.0".
func validateSchemaVersion(version string) error {
if version == "0.1.0" {
return nil
}
if version == "" {
return errors.New("plugin SchemaVersion version cannot be empty")
}
major, _, ok := strings.Cut(version, ".")
majorVersion, err := strconv.Atoi(major)
if !ok || err != nil {
return fmt.Errorf("plugin SchemaVersion %q has wrong format: must be <major>.<minor>.<patch>", version)
}
if majorVersion > 1 {
return fmt.Errorf("plugin SchemaVersion %q is not supported: must be lower than 2.0.0", version)
}
return nil
}
// RunHook executes the plugin's hooks command
// and returns its unprocessed output.
func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte, error) {
@ -170,26 +147,3 @@ func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte,
return hookCmdOutput, nil
}
// pluginNameFormat is used as part of errors for invalid plugin-names.
// We should consider making this less technical ("must start with "a-z",
// and only consist of lowercase alphanumeric characters").
const pluginNameFormat = `^[a-z][a-z0-9]*$`
func isValidPluginName(s string) bool {
if len(s) == 0 {
return false
}
// first character must be a-z
if c := s[0]; c < 'a' || c > 'z' {
return false
}
// followed by a-z or 0-9
for i := 1; i < len(s); i++ {
c := s[i]
if (c < 'a' || c > 'z') && (c < '0' || c > '9') {
return false
}
}
return true
}

View File

@ -33,6 +33,4 @@ type Metadata struct {
ShortDescription string `json:",omitempty"`
// URL is a pointer to the plugin's homepage.
URL string `json:",omitempty"`
// Hidden hides the plugin in completion and help message output.
Hidden bool `json:",omitempty"`
}

View File

@ -14,7 +14,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/connhelper"
"github.com/docker/cli/cli/debug"
"github.com/moby/moby/client"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
)
@ -80,23 +80,19 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
return cmd.Execute()
}
// Run is the top-level entry point to the CLI plugin framework. It should
// be called from the plugin's "main()" function. It initializes a new
// [command.DockerCli] instance with the given options before calling
// makeCmd to construct the plugin command, then invokes the plugin command
// using [RunPlugin].
func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata, ops ...command.CLIOption) {
// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) {
otel.SetErrorHandler(debug.OTELErrorHandler)
dockerCLI, err := command.NewDockerCli(ops...)
dockerCli, err := command.NewDockerCli()
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
plugin := makeCmd(dockerCLI)
plugin := makeCmd(dockerCli)
if err := RunPlugin(dockerCLI, plugin, meta); err != nil {
if err := RunPlugin(dockerCli, plugin, meta); err != nil {
var stErr cli.StatusError
if errors.As(err, &stErr) {
// StatusError should only be used for errors, and all errors should
@ -104,10 +100,10 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata, ops .
if stErr.StatusCode == 0 { // FIXME(thaJeztah): this should never be used with a zero status-code. Check if we do this anywhere.
stErr.StatusCode = 1
}
_, _ = fmt.Fprintln(dockerCLI.Err(), stErr)
_, _ = fmt.Fprintln(dockerCli.Err(), stErr)
os.Exit(stErr.StatusCode)
}
_, _ = fmt.Fprintln(dockerCLI.Err(), err)
_, _ = fmt.Fprintln(dockerCli.Err(), err)
os.Exit(1)
}
}
@ -139,7 +135,7 @@ func withPluginClientConn(name string) command.CLIOption {
if err != nil {
return err
}
apiClient, err := client.New(client.WithDialContext(helper.Dialer))
apiClient, err := client.NewClientWithOpts(client.WithDialContext(helper.Dialer))
if err != nil {
return err
}
@ -168,11 +164,6 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
DisableDescriptions: os.Getenv("DOCKER_CLI_DISABLE_COMPLETION_DESCRIPTION") != "",
},
}
// Disable file-completion by default. Most commands and flags should not
// complete with filenames.
cmd.CompletionOptions.SetDefaultShellCompDirective(cobra.ShellCompDirectiveNoFileComp)
opts, _ := cli.SetupPluginRootCommand(cmd)
cmd.SetIn(dockerCli.In())

View File

@ -12,6 +12,7 @@ import (
"github.com/fvbommel/sortorder"
"github.com/moby/term"
"github.com/morikuni/aec"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
@ -166,6 +167,31 @@ func (tcmd *TopLevelCommand) Initialize(ops ...command.CLIOption) error {
return tcmd.dockerCli.Initialize(tcmd.opts, ops...)
}
// VisitAll will traverse all commands from the root.
//
// Deprecated: this utility was only used internally and will be removed in the next release.
func VisitAll(root *cobra.Command, fn func(*cobra.Command)) {
visitAll(root, fn)
}
func visitAll(root *cobra.Command, fn func(*cobra.Command)) {
for _, cmd := range root.Commands() {
visitAll(cmd, fn)
}
fn(root)
}
// DisableFlagsInUseLine sets the DisableFlagsInUseLine flag on all
// commands within the tree rooted at cmd.
//
// Deprecated: this utility was only used internally and will be removed in the next release.
func DisableFlagsInUseLine(cmd *cobra.Command) {
visitAll(cmd, func(ccmd *cobra.Command) {
// do not add a `[flags]` to the end of the usage line.
ccmd.DisableFlagsInUseLine = true
})
}
var helpCommand = &cobra.Command{
Use: "help [command]",
Short: "Help about the command",
@ -174,7 +200,7 @@ var helpCommand = &cobra.Command{
RunE: func(c *cobra.Command, args []string) error {
cmd, args, e := c.Root().Find(args)
if cmd == nil || e != nil || len(args) > 0 {
return fmt.Errorf("unknown help topic: %v", strings.Join(args, " "))
return errors.Errorf("unknown help topic: %v", strings.Join(args, " "))
}
helpFunc := cmd.HelpFunc()
helpFunc(cmd, args)
@ -250,12 +276,11 @@ func commandAliases(cmd *cobra.Command) string {
if cmd.HasParent() {
parentPath = cmd.Parent().CommandPath() + " "
}
var aliases strings.Builder
aliases.WriteString(cmd.CommandPath())
aliases := cmd.CommandPath()
for _, alias := range cmd.Aliases {
aliases.WriteString(", " + parentPath + alias)
aliases += ", " + parentPath + alias
}
return aliases.String()
return aliases
}
func topCommands(cmd *cobra.Command) []*cobra.Command {
@ -351,10 +376,13 @@ func orchestratorSubCommands(cmd *cobra.Command) []*cobra.Command {
func allManagementSubCommands(cmd *cobra.Command) []*cobra.Command {
cmds := []*cobra.Command{}
for _, sub := range cmd.Commands() {
if invalidPluginReason(sub) != "" {
if isPlugin(sub) {
if invalidPluginReason(sub) == "" {
cmds = append(cmds, sub)
}
continue
}
if sub.IsAvailableCommand() && (isPlugin(sub) || sub.HasSubCommands()) {
if sub.IsAvailableCommand() && sub.HasSubCommands() {
cmds = append(cmds, sub)
}
}

View File

@ -56,33 +56,6 @@ func TestInvalidPlugin(t *testing.T) {
assert.DeepEqual(t, invalidPlugins(root), []*cobra.Command{sub1}, cmpopts.IgnoreUnexported(cobra.Command{}))
}
func TestHiddenPlugin(t *testing.T) {
root := &cobra.Command{Use: "root"}
sub1 := &cobra.Command{
Use: "sub1",
Hidden: true,
Annotations: map[string]string{
metadata.CommandAnnotationPlugin: "true",
},
Run: func(cmd *cobra.Command, args []string) {},
}
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
sub2 := &cobra.Command{
Use: "sub2",
Annotations: map[string]string{
metadata.CommandAnnotationPlugin: "true",
},
Run: func(cmd *cobra.Command, args []string) {},
}
root.AddCommand(sub1, sub2)
sub1.AddCommand(sub1sub1, sub1sub2)
assert.DeepEqual(t, allManagementSubCommands(root), []*cobra.Command{sub2}, cmpopts.IgnoreFields(cobra.Command{}, "Run"), cmpopts.IgnoreUnexported(cobra.Command{}))
}
func TestCommandAliases(t *testing.T) {
root := &cobra.Command{Use: "root"}
sub := &cobra.Command{Use: "subcommand", Aliases: []string{"alias1", "alias2"}}

View File

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

View File

@ -6,17 +6,15 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/internal/commands"
)
func init() {
commands.Register(newBuilderCommand)
commands.Register(func(c command.Cli) *cobra.Command {
return newBakeStubCommand(c)
})
// NewBuilderCommand returns a cobra command for `builder` subcommands
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewBuilderCommand(dockerCLI command.Cli) *cobra.Command {
return newBuilderCommand(dockerCLI)
}
// newBuilderCommand returns a cobra command for `builder` subcommands
func newBuilderCommand(dockerCLI command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "builder",
@ -24,8 +22,6 @@ func newBuilderCommand(dockerCLI command.Cli) *cobra.Command {
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCLI.Err()),
Annotations: map[string]string{"version": "1.31"},
DisableFlagsInUseLine: true,
}
cmd.AddCommand(
newPruneCommand(dockerCLI),
@ -36,10 +32,16 @@ func newBuilderCommand(dockerCLI command.Cli) *cobra.Command {
return cmd
}
// newBakeStubCommand returns a cobra command "stub" for the "bake" subcommand.
// NewBakeStubCommand returns a cobra command "stub" for the "bake" subcommand.
// This command is a placeholder / stub that is dynamically replaced by an
// alias for "docker buildx bake" if BuildKit is enabled (and the buildx plugin
// installed).
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
return newBakeStubCommand(dockerCLI)
}
func newBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
return &cobra.Command{
Use: "bake [OPTIONS] [TARGET...]",
@ -52,6 +54,5 @@ func newBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
"aliases": "docker buildx bake",
"version": "1.31",
},
DisableFlagsInUseLine: true,
}
}

View File

@ -8,26 +8,25 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/system/pruner"
"github.com/docker/cli/internal/prompt"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types/build"
"github.com/docker/go-units"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
func init() {
// Register the prune command to run as part of "docker system prune"
if err := pruner.Register(pruner.TypeBuildCache, pruneFn); err != nil {
panic(err)
}
type pruneOptions struct {
force bool
all bool
filter opts.FilterOpt
keepStorage opts.MemBytes
}
type pruneOptions struct {
force bool
all bool
filter opts.FilterOpt
reservedSpace opts.MemBytes
// NewPruneCommand returns a new cobra prune command for images
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
return newPruneCommand(dockerCli)
}
// newPruneCommand returns a new cobra prune command for images
@ -49,16 +48,15 @@ func newPruneCommand(dockerCLI command.Cli) *cobra.Command {
_, _ = fmt.Fprintln(dockerCLI.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
return nil
},
Annotations: map[string]string{"version": "1.39"},
ValidArgsFunction: cobra.NoFileCompletions,
DisableFlagsInUseLine: true,
Annotations: map[string]string{"version": "1.39"},
ValidArgsFunction: cobra.NoFileCompletions,
}
flags := cmd.Flags()
flags.BoolVarP(&options.force, "force", "f", false, "Do not prompt for confirmation")
flags.BoolVarP(&options.all, "all", "a", false, "Remove all unused build cache, not just dangling ones")
flags.Var(&options.filter, "filter", `Provide filter values (e.g. "until=24h")`)
flags.Var(&options.reservedSpace, "keep-storage", "Amount of disk space to keep for cache")
flags.Var(&options.keepStorage, "keep-storage", "Amount of disk space to keep for cache")
return cmd
}
@ -69,7 +67,8 @@ const (
)
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
pruneFilters := options.filter.Value()
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
warning := normalWarning
if options.all {
@ -85,15 +84,15 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
}
}
resp, err := dockerCli.Client().BuildCachePrune(ctx, client.BuildCachePruneOptions{
All: options.all,
ReservedSpace: options.reservedSpace.Value(),
Filters: pruneFilters,
report, err := dockerCli.Client().BuildCachePrune(ctx, build.CachePruneOptions{
All: options.all,
KeepStorage: options.keepStorage.Value(), // FIXME(thaJeztah): rewrite to use new options; see https://github.com/moby/moby/pull/48720
Filters: pruneFilters,
})
if err != nil {
return 0, "", err
}
report := resp.Report
if len(report.CachesDeleted) > 0 {
var sb strings.Builder
sb.WriteString("Deleted build cache objects:\n")
@ -111,22 +110,7 @@ type cancelledErr struct{ error }
func (cancelledErr) Cancelled() {}
// pruneFn prunes the build cache for use in "docker system prune" and
// returns the amount of space reclaimed and a detailed output string.
func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) {
if !options.Confirmed {
// Dry-run: perform validation and produce confirmation before pruning.
var confirmMsg string
if options.All {
confirmMsg = "all build cache"
} else {
confirmMsg = "unused build cache"
}
return 0, confirmMsg, cancelledErr{errors.New("builder prune has been cancelled")}
}
return runPrune(ctx, dockerCLI, pruneOptions{
force: true,
all: options.All,
filter: options.Filter,
})
// CachePrune executes a prune command for build cache
func CachePrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter})
}

View File

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

View File

@ -3,33 +3,34 @@ package checkpoint
import (
"context"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/checkpoint"
"github.com/docker/docker/client"
)
type fakeClient struct {
client.Client
checkpointCreateFunc func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error)
checkpointDeleteFunc func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error)
checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error)
checkpointCreateFunc func(container string, options checkpoint.CreateOptions) error
checkpointDeleteFunc func(container string, options checkpoint.DeleteOptions) error
checkpointListFunc func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error)
}
func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options checkpoint.CreateOptions) error {
if cli.checkpointCreateFunc != nil {
return cli.checkpointCreateFunc(container, options)
}
return client.CheckpointCreateResult{}, nil
return nil
}
func (cli *fakeClient) CheckpointRemove(_ context.Context, container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
func (cli *fakeClient) CheckpointDelete(_ context.Context, container string, options checkpoint.DeleteOptions) error {
if cli.checkpointDeleteFunc != nil {
return cli.checkpointDeleteFunc(container, options)
}
return client.CheckpointRemoveResult{}, nil
return nil
}
func (cli *fakeClient) CheckpointList(_ context.Context, container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
func (cli *fakeClient) CheckpointList(_ context.Context, container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
if cli.checkpointListFunc != nil {
return cli.checkpointListFunc(container, options)
}
return client.CheckpointListResult{}, nil
return []checkpoint.Summary{}, nil
}

View File

@ -3,15 +3,16 @@ package checkpoint
import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/commands"
"github.com/spf13/cobra"
)
func init() {
commands.Register(newCheckpointCommand)
// NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewCheckpointCommand(dockerCLI command.Cli) *cobra.Command {
return newCheckpointCommand(dockerCLI)
}
// newCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
func newCheckpointCommand(dockerCLI command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "checkpoint",
@ -23,7 +24,6 @@ func newCheckpointCommand(dockerCLI command.Cli) *cobra.Command {
"ostype": "linux",
"version": "1.25",
},
DisableFlagsInUseLine: true,
}
cmd.AddCommand(
newCreateCommand(dockerCLI),

View File

@ -6,7 +6,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/checkpoint"
"github.com/spf13/cobra"
)
@ -17,7 +17,7 @@ type createOptions struct {
leaveRunning bool
}
func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
func newCreateCommand(dockerCli command.Cli) *cobra.Command {
var opts createOptions
cmd := &cobra.Command{
@ -27,10 +27,9 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
opts.container = args[0]
opts.checkpoint = args[1]
return runCreate(cmd.Context(), dockerCLI, opts)
return runCreate(cmd.Context(), dockerCli, opts)
},
ValidArgsFunction: cobra.NoFileCompletions,
DisableFlagsInUseLine: true,
ValidArgsFunction: cobra.NoFileCompletions,
}
flags := cmd.Flags()
@ -41,7 +40,7 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
}
func runCreate(ctx context.Context, dockerCLI command.Cli, opts createOptions) error {
_, err := dockerCLI.Client().CheckpointCreate(ctx, opts.container, client.CheckpointCreateOptions{
err := dockerCLI.Client().CheckpointCreate(ctx, opts.container, checkpoint.CreateOptions{
CheckpointID: opts.checkpoint,
CheckpointDir: opts.checkpointDir,
Exit: !opts.leaveRunning,

View File

@ -8,7 +8,7 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/checkpoint"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
@ -16,7 +16,7 @@ import (
func TestCheckpointCreateErrors(t *testing.T) {
testCases := []struct {
args []string
checkpointCreateFunc func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error)
checkpointCreateFunc func(container string, options checkpoint.CreateOptions) error
expectedError string
}{
{
@ -29,8 +29,8 @@ func TestCheckpointCreateErrors(t *testing.T) {
},
{
args: []string{"foo", "bar"},
checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
return client.CheckpointCreateResult{}, errors.New("error creating checkpoint for container foo")
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
return errors.New("error creating checkpoint for container foo")
},
expectedError: "error creating checkpoint for container foo",
},
@ -59,12 +59,12 @@ func TestCheckpointCreateWithOptions(t *testing.T) {
leaveRunning := strconv.FormatBool(tc)
t.Run("leave-running="+leaveRunning, func(t *testing.T) {
var actualContainerName string
var actualOptions client.CheckpointCreateOptions
var actualOptions checkpoint.CreateOptions
cli := test.NewFakeCli(&fakeClient{
checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
actualContainerName = container
actualOptions = options
return client.CheckpointCreateResult{}, nil
return nil
},
})
cmd := newCreateCommand(cli)
@ -75,7 +75,7 @@ func TestCheckpointCreateWithOptions(t *testing.T) {
assert.Check(t, cmd.Flags().Set("checkpoint-dir", checkpointDir))
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Equal(actualContainerName, containerName))
expected := client.CheckpointCreateOptions{
expected := checkpoint.CreateOptions{
CheckpointID: checkpointName,
CheckpointDir: checkpointDir,
Exit: !tc,

View File

@ -2,7 +2,7 @@ package checkpoint
import (
"github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/checkpoint"
"github.com/docker/docker/api/types/checkpoint"
)
const (
@ -10,6 +10,13 @@ const (
checkpointNameHeader = "CHECKPOINT NAME"
)
// NewFormat returns a format for use with a checkpoint Context
//
// Deprecated: this function was only used internally and will be removed in the next release.
func NewFormat(source string) formatter.Format {
return newFormat(source)
}
// newFormat returns a format for use with a checkpointContext.
func newFormat(source string) formatter.Format {
if source == formatter.TableFormatKey {
@ -18,23 +25,24 @@ func newFormat(source string) formatter.Format {
return formatter.Format(source)
}
// FormatWrite writes formatted checkpoints using the Context
//
// Deprecated: this function was only used internally and will be removed in the next release.
func FormatWrite(fmtCtx formatter.Context, checkpoints []checkpoint.Summary) error {
return formatWrite(fmtCtx, checkpoints)
}
// formatWrite writes formatted checkpoints using the Context
func formatWrite(fmtCtx formatter.Context, checkpoints []checkpoint.Summary) error {
cpContext := &checkpointContext{
HeaderContext: formatter.HeaderContext{
Header: formatter.SubHeaderContext{
"Name": checkpointNameHeader,
},
},
}
return fmtCtx.Write(cpContext, func(format func(subContext formatter.SubContext) error) error {
render := func(format func(subContext formatter.SubContext) error) error {
for _, cp := range checkpoints {
if err := format(&checkpointContext{c: cp}); err != nil {
return err
}
}
return nil
})
}
return fmtCtx.Write(newCheckpointContext(), render)
}
type checkpointContext struct {
@ -42,6 +50,14 @@ type checkpointContext struct {
c checkpoint.Summary
}
func newCheckpointContext() *checkpointContext {
cpCtx := checkpointContext{}
cpCtx.Header = formatter.SubHeaderContext{
"Name": checkpointNameHeader,
}
return &cpCtx
}
func (c *checkpointContext) MarshalJSON() ([]byte, error) {
return formatter.MarshalJSON(c)
}

View File

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

View File

@ -7,7 +7,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/checkpoint"
"github.com/spf13/cobra"
)
@ -15,7 +15,7 @@ type listOptions struct {
checkpointDir string
}
func newListCommand(dockerCLI command.Cli) *cobra.Command {
func newListCommand(dockerCli command.Cli) *cobra.Command {
var opts listOptions
cmd := &cobra.Command{
@ -24,10 +24,9 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command {
Short: "List checkpoints for a container",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runList(cmd.Context(), dockerCLI, args[0], opts)
return runList(cmd.Context(), dockerCli, args[0], opts)
},
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
DisableFlagsInUseLine: true,
ValidArgsFunction: completion.ContainerNames(dockerCli, false),
}
flags := cmd.Flags()
@ -36,8 +35,8 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command {
return cmd
}
func runList(ctx context.Context, dockerCLI command.Cli, container string, opts listOptions) error {
checkpoints, err := dockerCLI.Client().CheckpointList(ctx, container, client.CheckpointListOptions{
func runList(ctx context.Context, dockerCli command.Cli, container string, opts listOptions) error {
checkpoints, err := dockerCli.Client().CheckpointList(ctx, container, checkpoint.ListOptions{
CheckpointDir: opts.checkpointDir,
})
if err != nil {
@ -45,8 +44,8 @@ func runList(ctx context.Context, dockerCLI command.Cli, container string, opts
}
cpCtx := formatter.Context{
Output: dockerCLI.Out(),
Output: dockerCli.Out(),
Format: newFormat(formatter.TableFormatKey),
}
return formatWrite(cpCtx, checkpoints.Items)
return formatWrite(cpCtx, checkpoints)
}

View File

@ -6,8 +6,7 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/checkpoint"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/checkpoint"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
@ -16,7 +15,7 @@ import (
func TestCheckpointListErrors(t *testing.T) {
testCases := []struct {
args []string
checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error)
checkpointListFunc func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error)
expectedError string
}{
{
@ -29,8 +28,8 @@ func TestCheckpointListErrors(t *testing.T) {
},
{
args: []string{"foo"},
checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
return client.CheckpointListResult{}, errors.New("error getting checkpoints for container foo")
checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
return []checkpoint.Summary{}, errors.New("error getting checkpoints for container foo")
},
expectedError: "error getting checkpoints for container foo",
},
@ -51,19 +50,17 @@ func TestCheckpointListErrors(t *testing.T) {
func TestCheckpointListWithOptions(t *testing.T) {
var containerID, checkpointDir string
cli := test.NewFakeCli(&fakeClient{
checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
containerID = container
checkpointDir = options.CheckpointDir
return client.CheckpointListResult{
Items: []checkpoint.Summary{
{Name: "checkpoint-foo"},
},
return []checkpoint.Summary{
{Name: "checkpoint-foo"},
}, nil
},
})
cmd := newListCommand(cli)
cmd.SetArgs([]string{"container-foo"})
assert.Check(t, cmd.Flags().Set("checkpoint-dir", "/dir/foo"))
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Equal("container-foo", containerID))
assert.Check(t, is.Equal("/dir/foo", checkpointDir))

View File

@ -1,9 +1,11 @@
package checkpoint
import (
"context"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/checkpoint"
"github.com/spf13/cobra"
)
@ -11,7 +13,7 @@ type removeOptions struct {
checkpointDir string
}
func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
var opts removeOptions
cmd := &cobra.Command{
@ -20,14 +22,8 @@ func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
Short: "Remove a checkpoint",
Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
containerID, checkpointID := args[0], args[1]
_, err := dockerCLI.Client().CheckpointRemove(cmd.Context(), containerID, client.CheckpointRemoveOptions{
CheckpointID: checkpointID,
CheckpointDir: opts.checkpointDir,
})
return err
return runRemove(cmd.Context(), dockerCli, args[0], args[1], opts)
},
DisableFlagsInUseLine: true,
}
flags := cmd.Flags()
@ -35,3 +31,10 @@ func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
return cmd
}
func runRemove(ctx context.Context, dockerCli command.Cli, container string, checkpointID string, opts removeOptions) error {
return dockerCli.Client().CheckpointDelete(ctx, container, checkpoint.DeleteOptions{
CheckpointID: checkpointID,
CheckpointDir: opts.checkpointDir,
})
}

View File

@ -6,7 +6,7 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/checkpoint"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
@ -14,7 +14,7 @@ import (
func TestCheckpointRemoveErrors(t *testing.T) {
testCases := []struct {
args []string
checkpointDeleteFunc func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error)
checkpointDeleteFunc func(container string, options checkpoint.DeleteOptions) error
expectedError string
}{
{
@ -27,8 +27,8 @@ func TestCheckpointRemoveErrors(t *testing.T) {
},
{
args: []string{"foo", "bar"},
checkpointDeleteFunc: func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
return client.CheckpointRemoveResult{}, errors.New("error deleting checkpoint")
checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
return errors.New("error deleting checkpoint")
},
expectedError: "error deleting checkpoint",
},
@ -49,16 +49,16 @@ func TestCheckpointRemoveErrors(t *testing.T) {
func TestCheckpointRemoveWithOptions(t *testing.T) {
var containerID, checkpointID, checkpointDir string
cli := test.NewFakeCli(&fakeClient{
checkpointDeleteFunc: func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
containerID = container
checkpointID = options.CheckpointID
checkpointDir = options.CheckpointDir
return client.CheckpointRemoveResult{}, nil
return nil
},
})
cmd := newRemoveCommand(cli)
cmd.SetArgs([]string{"container-foo", "checkpoint-bar"})
assert.Check(t, cmd.Flags().Set("checkpoint-dir", "/dir/foo"))
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Equal("container-foo", containerID))
assert.Check(t, is.Equal("checkpoint-bar", checkpointID))

View File

@ -1,11 +1,10 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.23
package command
import (
"context"
"errors"
"fmt"
"io"
"os"
@ -24,8 +23,11 @@ import (
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/version"
dopts "github.com/docker/cli/opts"
"github.com/moby/moby/api/types/build"
"github.com/moby/moby/client"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types/build"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -43,9 +45,12 @@ type Cli interface {
Client() client.APIClient
Streams
SetIn(in *streams.In)
Apply(ops ...CLIOption) error
config.Provider
ServerInfo() ServerInfo
DefaultVersion() string
CurrentVersion() string
ContentTrustEnabled() bool
BuildKitEnabled() (bool, error)
ContextStore() store.Store
CurrentContext() string
@ -65,6 +70,7 @@ type DockerCli struct {
err *streams.Out
client client.APIClient
serverInfo ServerInfo
contentTrust bool
contextStore store.Store
currentContext string
init sync.Once
@ -72,7 +78,6 @@ type DockerCli struct {
dockerEndpoint docker.Endpoint
contextStoreConfig *store.Config
initTimeout time.Duration
userAgent string
res telemetryResource
// baseCtx is the base context used for internal operations. In the future
@ -83,12 +88,17 @@ type DockerCli struct {
enableGlobalMeter, enableGlobalTracer bool
}
// DefaultVersion returns [api.DefaultVersion].
func (*DockerCli) DefaultVersion() string {
return api.DefaultVersion
}
// CurrentVersion returns the API version currently negotiated, or the default
// version otherwise.
func (cli *DockerCli) CurrentVersion() string {
_ = cli.initialize()
if cli.client == nil {
return client.MaxAPIVersion
return api.DefaultVersion
}
return cli.client.ClientVersion()
}
@ -147,13 +157,19 @@ func (cli *DockerCli) ServerInfo() ServerInfo {
return cli.serverInfo
}
// ContentTrustEnabled returns whether content trust has been enabled by an
// environment variable.
func (cli *DockerCli) ContentTrustEnabled() bool {
return cli.contentTrust
}
// BuildKitEnabled returns buildkit is enabled or not.
func (cli *DockerCli) BuildKitEnabled() (bool, error) {
// use DOCKER_BUILDKIT env var value if set and not empty
if v := os.Getenv("DOCKER_BUILDKIT"); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false, fmt.Errorf("DOCKER_BUILDKIT environment variable expects boolean value: %w", err)
return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
}
return enabled, nil
}
@ -253,7 +269,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
cli.contextStore = &ContextStoreWithDefault{
Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig),
Resolver: func() (*DefaultContext, error) {
return resolveDefaultContext(cli.options, *cli.contextStoreConfig)
return ResolveDefaultContext(cli.options, *cli.contextStoreConfig)
},
}
@ -290,17 +306,17 @@ func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.
contextStore := &ContextStoreWithDefault{
Store: store.New(config.ContextStoreDir(), storeConfig),
Resolver: func() (*DefaultContext, error) {
return resolveDefaultContext(opts, storeConfig)
return ResolveDefaultContext(opts, storeConfig)
},
}
endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile))
if err != nil {
return nil, fmt.Errorf("unable to resolve docker endpoint: %w", err)
return nil, errors.Wrap(err, "unable to resolve docker endpoint")
}
return newAPIClientFromEndpoint(endpoint, configFile, client.WithUserAgent(UserAgent()))
return newAPIClientFromEndpoint(endpoint, configFile)
}
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile, extraOpts ...client.Opt) (client.APIClient, error) {
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
opts, err := ep.ClientOpts()
if err != nil {
return nil, err
@ -308,15 +324,8 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
if len(configFile.HTTPHeaders) > 0 {
opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders))
}
withCustomHeaders, err := withCustomHeadersFromEnv()
if err != nil {
return nil, err
}
if withCustomHeaders != nil {
opts = append(opts, withCustomHeaders)
}
opts = append(opts, extraOpts...)
return client.New(opts...)
opts = append(opts, withCustomHeadersFromEnv(), client.WithUserAgent(UserAgent()))
return client.NewClientWithOpts(opts...)
}
func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) {
@ -377,21 +386,24 @@ func (cli *DockerCli) initializeFromClient() {
ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout())
defer cancel()
ping, err := cli.client.Ping(ctx, client.PingOptions{
NegotiateAPIVersion: true,
ForceNegotiate: true,
})
ping, err := cli.client.Ping(ctx)
if err != nil {
// Default to true if we fail to connect to daemon
cli.serverInfo = ServerInfo{HasExperimental: true}
if ping.APIVersion != "" {
cli.client.NegotiateAPIVersionPing(ping)
}
return
}
cli.serverInfo = ServerInfo{
HasExperimental: ping.Experimental,
OSType: ping.OSType,
BuildkitVersion: ping.BuilderVersion,
SwarmStatus: ping.SwarmStatus,
}
cli.client.NegotiateAPIVersionPing(ping)
}
// ContextStore returns the ContextStore
@ -529,12 +541,11 @@ func (cli *DockerCli) initialize() error {
cli.init.Do(func() {
cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint()
if cli.initErr != nil {
cli.initErr = fmt.Errorf("unable to resolve docker endpoint: %w", cli.initErr)
cli.initErr = errors.Wrap(cli.initErr, "unable to resolve docker endpoint")
return
}
if cli.client == nil {
ops := []client.Opt{client.WithUserAgent(cli.userAgent)}
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile, ops...); cli.initErr != nil {
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil {
return
}
}
@ -546,6 +557,16 @@ func (cli *DockerCli) initialize() error {
return cli.initErr
}
// Apply all the operation on the cli
func (cli *DockerCli) Apply(ops ...CLIOption) error {
for _, op := range ops {
if err := op(cli); err != nil {
return err
}
}
return nil
}
// ServerInfo stores details about the supported features and platform of the
// server
type ServerInfo struct {
@ -560,7 +581,7 @@ type ServerInfo struct {
// in the ping response, or if an error occurred, in which case the client
// should use other ways to get the current swarm status, such as the /swarm
// endpoint.
SwarmStatus *client.SwarmStatus
SwarmStatus *swarm.Status
}
// NewDockerCli returns a DockerCli instance with all operators applied on it.
@ -568,17 +589,15 @@ type ServerInfo struct {
// environment.
func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
defaultOps := []CLIOption{
WithContentTrustFromEnv(),
WithDefaultContextStoreConfig(),
WithStandardStreams(),
WithUserAgent(UserAgent()),
}
ops = append(defaultOps, ops...)
cli := &DockerCli{baseCtx: context.Background()}
for _, op := range ops {
if err := op(cli); err != nil {
return nil, err
}
if err := cli.Apply(ops...); err != nil {
return nil, err
}
return cli, nil
}
@ -590,11 +609,11 @@ func getServerHost(hosts []string, defaultToTLS bool) (string, error) {
case 1:
return dopts.ParseHost(defaultToTLS, hosts[0])
default:
return "", errors.New("specify only one -H")
return "", errors.New("Specify only one -H")
}
}
// UserAgent returns the default user agent string used for making API requests.
// UserAgent returns the user agent string used for making API requests
func UserAgent() string {
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
}

View File

@ -3,16 +3,16 @@ package command
import (
"context"
"encoding/csv"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"github.com/docker/cli/cli/streams"
"github.com/moby/moby/client"
"github.com/docker/docker/client"
"github.com/moby/term"
"github.com/pkg/errors"
)
// CLIOption is a functional argument to apply options to a [DockerCli]. These
@ -75,6 +75,28 @@ func WithErrorStream(err io.Writer) CLIOption {
}
}
// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
func WithContentTrustFromEnv() CLIOption {
return func(cli *DockerCli) error {
cli.contentTrust = false
if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" {
if t, err := strconv.ParseBool(e); t || err != nil {
// treat any other value as true
cli.contentTrust = true
}
}
return nil
}
}
// WithContentTrust enables content trust on a cli.
func WithContentTrust(enabled bool) CLIOption {
return func(cli *DockerCli) error {
cli.contentTrust = enabled
return nil
}
}
// WithDefaultContextStoreConfig configures the cli to use the default context store configuration.
func WithDefaultContextStoreConfig() CLIOption {
return func(cli *DockerCli) error {
@ -158,70 +180,61 @@ const envOverrideHTTPHeaders = "DOCKER_CUSTOM_HEADERS"
// override headers with the same name).
//
// TODO(thaJeztah): this is a client Option, and should be moved to the client. It is non-exported for that reason.
func withCustomHeadersFromEnv() (client.Opt, error) {
value := os.Getenv(envOverrideHTTPHeaders)
if value == "" {
return nil, nil
}
csvReader := csv.NewReader(strings.NewReader(value))
fields, err := csvReader.Read()
if err != nil {
return nil, invalidParameter(fmt.Errorf(
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
envOverrideHTTPHeaders,
))
}
if len(fields) == 0 {
return nil, nil
}
env := map[string]string{}
for _, kv := range fields {
k, v, hasValue := strings.Cut(kv, "=")
// Only strip whitespace in keys; preserve whitespace in values.
k = strings.TrimSpace(k)
if k == "" {
return nil, invalidParameter(fmt.Errorf(
`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`,
envOverrideHTTPHeaders, kv,
func withCustomHeadersFromEnv() client.Opt {
return func(apiClient *client.Client) error {
value := os.Getenv(envOverrideHTTPHeaders)
if value == "" {
return nil
}
csvReader := csv.NewReader(strings.NewReader(value))
fields, err := csvReader.Read()
if err != nil {
return invalidParameter(errors.Errorf(
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
envOverrideHTTPHeaders,
))
}
// We don't currently allow empty key=value pairs, and produce an error.
// This is something we could allow in future (e.g. to read value
// from an environment variable with the same name). In the meantime,
// produce an error to prevent users from depending on this.
if !hasValue {
return nil, invalidParameter(fmt.Errorf(
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
envOverrideHTTPHeaders, kv,
))
if len(fields) == 0 {
return nil
}
env[http.CanonicalHeaderKey(k)] = v
}
env := map[string]string{}
for _, kv := range fields {
k, v, hasValue := strings.Cut(kv, "=")
if len(env) == 0 {
// We should probably not hit this case, as we don't skip values
// (only return errors), but we don't want to discard existing
// headers with an empty set.
return nil, nil
}
// Only strip whitespace in keys; preserve whitespace in values.
k = strings.TrimSpace(k)
// TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
return client.WithHTTPHeaders(env), nil
}
if k == "" {
return invalidParameter(errors.Errorf(
`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`,
envOverrideHTTPHeaders, kv,
))
}
// WithUserAgent configures the User-Agent string for cli HTTP requests.
func WithUserAgent(userAgent string) CLIOption {
return func(cli *DockerCli) error {
if userAgent == "" {
return errors.New("user agent cannot be blank")
// We don't currently allow empty key=value pairs, and produce an error.
// This is something we could allow in future (e.g. to read value
// from an environment variable with the same name). In the meantime,
// produce an error to prevent users from depending on this.
if !hasValue {
return invalidParameter(errors.Errorf(
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
envOverrideHTTPHeaders, kv,
))
}
env[http.CanonicalHeaderKey(k)] = v
}
cli.userAgent = userAgent
return nil
if len(env) == 0 {
// We should probably not hit this case, as we don't skip values
// (only return errors), but we don't want to discard existing
// headers with an empty set.
return nil
}
// TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
return client.WithHTTPHeaders(env)(apiClient)
}
}

View File

@ -0,0 +1,28 @@
package command
import (
"os"
"testing"
"gotest.tools/v3/assert"
)
func contentTrustEnabled(t *testing.T) bool {
t.Helper()
var cli DockerCli
assert.NilError(t, WithContentTrustFromEnv()(&cli))
return cli.contentTrust
}
// NB: Do not t.Parallel() this test -- it messes with the process environment.
func TestWithContentTrustFromEnv(t *testing.T) {
const envvar = "DOCKER_CONTENT_TRUST"
t.Setenv(envvar, "true")
assert.Check(t, contentTrustEnabled(t))
t.Setenv(envvar, "false")
assert.Check(t, !contentTrustEnabled(t))
t.Setenv(envvar, "invalid")
assert.Check(t, contentTrustEnabled(t))
os.Unsetenv(envvar)
assert.Check(t, !contentTrustEnabled(t))
}

View File

@ -20,7 +20,9 @@ import (
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/context/store"
"github.com/docker/cli/cli/flags"
"github.com/moby/moby/client"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"gotest.tools/v3/assert"
)
@ -33,7 +35,7 @@ func TestNewAPIClientFromFlags(t *testing.T) {
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host)
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
}
func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
@ -46,7 +48,7 @@ func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), slug+host)
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
}
func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
@ -70,7 +72,7 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
apiClient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host)
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
// verify User-Agent is not appended to the configfile. see https://github.com/docker/cli/pull/2756
assert.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"})
@ -79,7 +81,7 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
"My-Header": "Custom-Value",
"User-Agent": UserAgent(),
}
_, err = apiClient.Ping(context.TODO(), client.PingOptions{})
_, err = apiClient.Ping(context.Background())
assert.NilError(t, err)
assert.DeepEqual(t, received, expectedHeaders)
}
@ -105,7 +107,7 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
apiClient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host)
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
expectedHeaders := http.Header{
"One": []string{"one-value"},
@ -114,14 +116,14 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
"Four": []string{"four-value-override"},
"User-Agent": []string{UserAgent()},
}
_, err = apiClient.Ping(context.TODO(), client.PingOptions{})
_, err = apiClient.Ping(context.Background())
assert.NilError(t, err)
assert.DeepEqual(t, received, expectedHeaders)
}
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
const customVersion = "v3.3"
const expectedVersion = "3.3"
const customVersion = "v3.3.3"
const expectedVersion = "3.3.3"
t.Setenv("DOCKER_API_VERSION", customVersion)
t.Setenv("DOCKER_HOST", ":2375")
@ -134,55 +136,51 @@ func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
type fakeClient struct {
client.Client
pingFunc func() (client.PingResult, error)
pingFunc func() (types.Ping, error)
version string
negotiated bool
}
func (c *fakeClient) Ping(_ context.Context, options client.PingOptions) (client.PingResult, error) {
res, err := c.pingFunc()
if options.NegotiateAPIVersion {
if res.APIVersion != "" {
if c.negotiated || options.ForceNegotiate {
c.negotiated = true
}
}
}
return res, err
func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) {
return c.pingFunc()
}
func (c *fakeClient) ClientVersion() string {
return c.version
}
func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
c.negotiated = true
}
func TestInitializeFromClient(t *testing.T) {
const defaultVersion = "v1.55"
testcases := []struct {
doc string
pingFunc func() (client.PingResult, error)
pingFunc func() (types.Ping, error)
expectedServer ServerInfo
negotiated bool
}{
{
doc: "successful ping",
pingFunc: func() (client.PingResult, error) {
return client.PingResult{Experimental: true, OSType: "linux", APIVersion: "v1.44"}, nil
pingFunc: func() (types.Ping, error) {
return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil
},
expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
negotiated: true,
},
{
doc: "failed ping, no API version",
pingFunc: func() (client.PingResult, error) {
return client.PingResult{}, errors.New("failed")
pingFunc: func() (types.Ping, error) {
return types.Ping{}, errors.New("failed")
},
expectedServer: ServerInfo{HasExperimental: true},
},
{
doc: "failed ping, with API version",
pingFunc: func() (client.PingResult, error) {
return client.PingResult{APIVersion: "v1.44"}, errors.New("failed")
pingFunc: func() (types.Ping, error) {
return types.Ping{APIVersion: "v1.33"}, errors.New("failed")
},
expectedServer: ServerInfo{HasExperimental: true},
negotiated: true,
@ -214,7 +212,7 @@ func TestInitializeFromClientHangs(t *testing.T) {
assert.NilError(t, err)
receiveReqCh := make(chan bool)
timeoutCtx, cancel := context.WithTimeout(context.TODO(), time.Second)
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Simulate a server that hangs on connections.
@ -258,14 +256,8 @@ func TestInitializeFromClientHangs(t *testing.T) {
}
func TestNewDockerCliAndOperators(t *testing.T) {
outbuf := bytes.NewBuffer(nil)
errbuf := bytes.NewBuffer(nil)
cli, err := NewDockerCli(
WithInputStream(io.NopCloser(strings.NewReader("some input"))),
WithOutputStream(outbuf),
WithErrorStream(errbuf),
)
// Test default operations and also overriding default ones
cli, err := NewDockerCli(WithInputStream(io.NopCloser(strings.NewReader("some input"))))
assert.NilError(t, err)
// Check streams are initialized
assert.Check(t, cli.In() != nil)
@ -275,6 +267,19 @@ func TestNewDockerCliAndOperators(t *testing.T) {
assert.NilError(t, err)
assert.Equal(t, string(inputStream), "some input")
// Apply can modify a dockerCli after construction
outbuf := bytes.NewBuffer(nil)
errbuf := bytes.NewBuffer(nil)
err = cli.Apply(
WithInputStream(io.NopCloser(strings.NewReader("input"))),
WithOutputStream(outbuf),
WithErrorStream(errbuf),
)
assert.NilError(t, err)
// Check input stream
inputStream, err = io.ReadAll(cli.In())
assert.NilError(t, err)
assert.Equal(t, string(inputStream), "input")
// Check output stream
_, err = fmt.Fprint(cli.Out(), "output")
assert.NilError(t, err)
@ -292,9 +297,9 @@ func TestNewDockerCliAndOperators(t *testing.T) {
func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
cli, err := NewDockerCli()
assert.NilError(t, err)
apiClient, err := client.New()
assert.NilError(t, err)
assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithAPIClient(apiClient)))
assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithInitializeClient(func(cli *DockerCli) (client.APIClient, error) {
return client.NewClientWithOpts()
})))
assert.Check(t, cli.ContextStore() != nil)
}
@ -369,26 +374,3 @@ func TestSetGoDebug(t *testing.T) {
assert.Equal(t, "val1,val2=1", os.Getenv("GODEBUG"))
})
}
func TestNewDockerCliWithCustomUserAgent(t *testing.T) {
var received string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
received = r.UserAgent()
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
opts := &flags.ClientOptions{Hosts: []string{host}}
cli, err := NewDockerCli(
WithUserAgent("fake-agent/0.0.1"),
)
assert.NilError(t, err)
cli.currentContext = DefaultContextName
cli.options = opts
cli.configFile = &configfile.ConfigFile{}
_, err = cli.Client().Ping(context.TODO(), client.PingOptions{})
assert.NilError(t, err)
assert.DeepEqual(t, received, "fake-agent/0.0.1")
}

View File

@ -1,30 +1,168 @@
package commands
import (
"os"
"github.com/docker/cli/cli/command"
_ "github.com/docker/cli/cli/command/builder"
_ "github.com/docker/cli/cli/command/checkpoint"
_ "github.com/docker/cli/cli/command/config"
_ "github.com/docker/cli/cli/command/container"
_ "github.com/docker/cli/cli/command/context"
_ "github.com/docker/cli/cli/command/image"
_ "github.com/docker/cli/cli/command/manifest"
_ "github.com/docker/cli/cli/command/network"
_ "github.com/docker/cli/cli/command/node"
_ "github.com/docker/cli/cli/command/plugin"
_ "github.com/docker/cli/cli/command/registry"
_ "github.com/docker/cli/cli/command/secret"
_ "github.com/docker/cli/cli/command/service"
_ "github.com/docker/cli/cli/command/stack"
_ "github.com/docker/cli/cli/command/swarm"
_ "github.com/docker/cli/cli/command/system"
_ "github.com/docker/cli/cli/command/volume"
"github.com/docker/cli/internal/commands"
"github.com/docker/cli/cli/command/builder"
"github.com/docker/cli/cli/command/checkpoint"
"github.com/docker/cli/cli/command/config"
"github.com/docker/cli/cli/command/container"
"github.com/docker/cli/cli/command/context"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/command/manifest"
"github.com/docker/cli/cli/command/network"
"github.com/docker/cli/cli/command/node"
"github.com/docker/cli/cli/command/plugin"
"github.com/docker/cli/cli/command/registry"
"github.com/docker/cli/cli/command/secret"
"github.com/docker/cli/cli/command/service"
"github.com/docker/cli/cli/command/stack"
"github.com/docker/cli/cli/command/swarm"
"github.com/docker/cli/cli/command/system"
"github.com/docker/cli/cli/command/trust"
"github.com/docker/cli/cli/command/volume"
"github.com/spf13/cobra"
)
func AddCommands(cmd *cobra.Command, dockerCLI command.Cli) {
for _, c := range commands.Commands() {
cmd.AddCommand(c(dockerCLI))
}
// AddCommands adds all the commands from cli/command to the root command
func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
cmd.AddCommand(
// commonly used shorthands
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
container.NewRunCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
container.NewExecCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
container.NewPsCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
image.NewBuildCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
image.NewPullCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
image.NewPushCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
image.NewImagesCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
registry.NewLoginCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
registry.NewLogoutCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
registry.NewSearchCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
system.NewVersionCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
system.NewInfoCommand(dockerCli),
// management commands
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
builder.NewBakeStubCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
builder.NewBuilderCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
checkpoint.NewCheckpointCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
container.NewContainerCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
context.NewContextCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
image.NewImageCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
manifest.NewManifestCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
network.NewNetworkCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
plugin.NewPluginCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
system.NewSystemCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
trust.NewTrustCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
volume.NewVolumeCommand(dockerCli),
// orchestration (swarm) commands
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
config.NewConfigCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
node.NewNodeCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
secret.NewSecretCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
service.NewServiceCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
stack.NewStackCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
swarm.NewSwarmCommand(dockerCli),
// legacy commands may be hidden
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewAttachCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewCommitCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewCopyCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewCreateCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewDiffCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewExportCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewKillCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewLogsCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewPauseCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewPortCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewRenameCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewRestartCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewRmCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewStartCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewStatsCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewStopCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewTopCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewUnpauseCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewUpdateCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(container.NewWaitCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(image.NewHistoryCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(image.NewImportCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(image.NewLoadCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(image.NewRemoveCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(image.NewSaveCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(image.NewTagCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(system.NewEventsCommand(dockerCli)),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
hide(system.NewInspectCommand(dockerCli)),
)
}
func hide(cmd *cobra.Command) *cobra.Command {
// If the environment variable with name "DOCKER_HIDE_LEGACY_COMMANDS" is not empty,
// these legacy commands (such as `docker ps`, `docker exec`, etc)
// will not be shown in output console.
if os.Getenv("DOCKER_HIDE_LEGACY_COMMANDS") == "" {
return cmd
}
cmdCopy := *cmd
cmdCopy.Hidden = true
cmdCopy.Aliases = []string{}
return &cmdCopy
}

View File

@ -4,14 +4,16 @@ import (
"os"
"strings"
"github.com/distribution/reference"
"github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
)
// APIClientProvider provides a method to get a [client.APIClient], initializing
// APIClientProvider provides a method to get an [client.APIClient], initializing
// it if needed.
//
// It's a smaller interface than [command.Cli], and used in situations where an
@ -27,57 +29,24 @@ func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
if limit > 0 && len(args) >= limit {
return nil, cobra.ShellCompDirectiveNoFileComp
}
res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{})
list, err := dockerCLI.Client().ImageList(cmd.Context(), image.ListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
for _, img := range res.Items {
for _, img := range list {
names = append(names, img.RepoTags...)
}
return names, cobra.ShellCompDirectiveNoFileComp
}
}
// ImageNamesWithBase offers completion for images present within the local store,
// including both full image names with tags and base image names (repository names only)
// when multiple tags exist for the same base name
func ImageNamesWithBase(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if limit > 0 && len(args) >= limit {
return nil, cobra.ShellCompDirectiveNoFileComp
}
res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
baseNameCounts := make(map[string]int)
for _, img := range res.Items {
names = append(names, img.RepoTags...)
for _, tag := range img.RepoTags {
ref, err := reference.ParseNormalizedNamed(tag)
if err != nil {
continue
}
baseNameCounts[reference.FamiliarName(ref)]++
}
}
for baseName, count := range baseNameCounts {
if count > 1 {
names = append(names, baseName)
}
}
return names, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
}
}
// ContainerNames offers completion for container names and IDs
// By default, only names are returned.
// Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs.
func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(container.Summary) bool) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().ContainerList(cmd.Context(), client.ContainerListOptions{
list, err := dockerCLI.Client().ContainerList(cmd.Context(), container.ListOptions{
All: all,
})
if err != nil {
@ -87,7 +56,7 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
showContainerIDs := os.Getenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS") == "yes"
var names []string
for _, ctr := range res.Items {
for _, ctr := range list {
skip := false
for _, fn := range filters {
if fn != nil && !fn(ctr) {
@ -110,12 +79,12 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
// VolumeNames offers completion for volumes
func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().VolumeList(cmd.Context(), client.VolumeListOptions{})
list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
for _, vol := range res.Items {
for _, vol := range list.Volumes {
names = append(names, vol.Name)
}
return names, cobra.ShellCompDirectiveNoFileComp
@ -125,12 +94,12 @@ func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
// NetworkNames offers completion for networks
func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().NetworkList(cmd.Context(), client.NetworkListOptions{})
list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
for _, nw := range res.Items {
for _, nw := range list {
names = append(names, nw.Name)
}
return names, cobra.ShellCompDirectiveNoFileComp
@ -150,16 +119,14 @@ func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
// export MY_VAR=hello
// docker run --rm --env MY_VAR alpine printenv MY_VAR
// hello
func EnvVarNames() cobra.CompletionFunc {
return func(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
envs := os.Environ()
names = make([]string, 0, len(envs))
for _, env := range envs {
name, _, _ := strings.Cut(env, "=")
names = append(names, name)
}
return names, cobra.ShellCompDirectiveNoFileComp
func EnvVarNames(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
envs := os.Environ()
names = make([]string, 0, len(envs))
for _, env := range envs {
name, _, _ := strings.Cut(env, "=")
names = append(names, name)
}
return names, cobra.ShellCompDirectiveNoFileComp
}
// FromList offers completion for the given list of options.
@ -170,10 +137,15 @@ func FromList(options ...string) cobra.CompletionFunc {
// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault],
// which indicates to let the shell perform its default behavior after
// completions have been provided.
func FileNames() cobra.CompletionFunc {
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveDefault
}
func FileNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveDefault
}
// NoComplete is used for commands where there's no relevant completion.
//
// Deprecated: use [cobra.NoFileCompletions]. This function will be removed in the next release.
func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveNoFileComp
}
var commonPlatforms = []string{
@ -213,8 +185,6 @@ var commonPlatforms = []string{
// - we currently exclude architectures that may have unofficial builds,
// but don't have wide adoption (and no support), such as loong64, mipsXXX,
// ppc64 (non-le) to prevent confusion.
func Platforms() cobra.CompletionFunc {
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
}
func Platforms(_ *cobra.Command, _ []string, _ string) (platforms []string, _ cobra.ShellCompDirective) {
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
}

View File

@ -6,11 +6,13 @@ import (
"sort"
"testing"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/volume"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
@ -28,38 +30,38 @@ func (c fakeCLI) Client() client.APIClient {
type fakeClient struct {
client.Client
containerListFunc func(context.Context, client.ContainerListOptions) (client.ContainerListResult, error)
imageListFunc func(context.Context, client.ImageListOptions) (client.ImageListResult, error)
networkListFunc func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error)
volumeListFunc func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error)
containerListFunc func(options container.ListOptions) ([]container.Summary, error)
imageListFunc func(options image.ListOptions) ([]image.Summary, error)
networkListFunc func(ctx context.Context, options network.ListOptions) ([]network.Summary, error)
volumeListFunc func(filter filters.Args) (volume.ListResponse, error)
}
func (c *fakeClient) ContainerList(ctx context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) {
func (c *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]container.Summary, error) {
if c.containerListFunc != nil {
return c.containerListFunc(ctx, options)
return c.containerListFunc(options)
}
return client.ContainerListResult{}, nil
return []container.Summary{}, nil
}
func (c *fakeClient) ImageList(ctx context.Context, options client.ImageListOptions) (client.ImageListResult, error) {
func (c *fakeClient) ImageList(_ context.Context, options image.ListOptions) ([]image.Summary, error) {
if c.imageListFunc != nil {
return c.imageListFunc(ctx, options)
return c.imageListFunc(options)
}
return client.ImageListResult{}, nil
return []image.Summary{}, nil
}
func (c *fakeClient) NetworkList(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) {
func (c *fakeClient) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
if c.networkListFunc != nil {
return c.networkListFunc(ctx, options)
}
return client.NetworkListResult{}, nil
return []network.Inspect{}, nil
}
func (c *fakeClient) VolumeList(ctx context.Context, options client.VolumeListOptions) (client.VolumeListResult, error) {
func (c *fakeClient) VolumeList(_ context.Context, options volume.ListOptions) (volume.ListResponse, error) {
if c.volumeListFunc != nil {
return c.volumeListFunc(ctx, options)
return c.volumeListFunc(options.Filters)
}
return client.VolumeListResult{}, nil
return volume.ListResponse{}, nil
}
func TestCompleteContainerNames(t *testing.T) {
@ -69,7 +71,7 @@ func TestCompleteContainerNames(t *testing.T) {
filters []func(container.Summary) bool
containers []container.Summary
expOut []string
expOpts client.ContainerListOptions
expOpts container.ListOptions
expDirective cobra.ShellCompDirective
}{
{
@ -85,7 +87,7 @@ func TestCompleteContainerNames(t *testing.T) {
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
},
expOut: []string{"container-c", "container-c/link-b", "container-b", "container-a"},
expOpts: client.ContainerListOptions{All: true},
expOpts: container.ListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
@ -98,7 +100,7 @@ func TestCompleteContainerNames(t *testing.T) {
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
},
expOut: []string{"id-c", "container-c", "container-c/link-b", "id-b", "container-b", "id-a", "container-a"},
expOpts: client.ContainerListOptions{All: true},
expOpts: container.ListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
@ -122,7 +124,7 @@ func TestCompleteContainerNames(t *testing.T) {
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
},
expOut: []string{"container-b"},
expOpts: client.ContainerListOptions{All: true},
expOpts: container.ListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
@ -138,7 +140,7 @@ func TestCompleteContainerNames(t *testing.T) {
{ID: "id-a", State: container.StateCreated, Names: []string{"/container-a"}},
},
expOut: []string{"container-a"},
expOpts: client.ContainerListOptions{All: true},
expOpts: container.ListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
@ -153,12 +155,12 @@ func TestCompleteContainerNames(t *testing.T) {
t.Setenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS", "yes")
}
comp := ContainerNames(fakeCLI{&fakeClient{
containerListFunc: func(_ context.Context, opts client.ContainerListOptions) (client.ContainerListResult, error) {
assert.Check(t, is.DeepEqual(opts, tc.expOpts))
containerListFunc: func(opts container.ListOptions) ([]container.Summary, error) {
assert.Check(t, is.DeepEqual(opts, tc.expOpts, cmpopts.IgnoreUnexported(container.ListOptions{}, filters.Args{})))
if tc.expDirective == cobra.ShellCompDirectiveError {
return client.ContainerListResult{}, errors.New("some error occurred")
return nil, errors.New("some error occurred")
}
return client.ContainerListResult{Items: tc.containers}, nil
return tc.containers, nil
},
}}, tc.showAll, tc.filters...)
@ -174,7 +176,7 @@ func TestCompleteEnvVarNames(t *testing.T) {
"ENV_A": "hello-a",
"ENV_B": "hello-b",
})
values, directives := EnvVarNames()(nil, nil, "")
values, directives := EnvVarNames(nil, nil, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
sort.Strings(values)
@ -183,7 +185,7 @@ func TestCompleteEnvVarNames(t *testing.T) {
}
func TestCompleteFileNames(t *testing.T) {
values, directives := FileNames()(nil, nil, "")
values, directives := FileNames(nil, nil, "")
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveDefault))
assert.Check(t, is.Len(values, 0))
}
@ -226,11 +228,11 @@ func TestCompleteImageNames(t *testing.T) {
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
comp := ImageNames(fakeCLI{&fakeClient{
imageListFunc: func(context.Context, client.ImageListOptions) (client.ImageListResult, error) {
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
if tc.expDirective == cobra.ShellCompDirectiveError {
return client.ImageListResult{}, errors.New("some error occurred")
return nil, errors.New("some error occurred")
}
return client.ImageListResult{Items: tc.images}, nil
return tc.images, nil
},
}}, -1)
@ -255,24 +257,9 @@ func TestCompleteNetworkNames(t *testing.T) {
{
doc: "with results",
networks: []network.Summary{
{
Network: network.Network{
ID: "nw-c",
Name: "network-c",
},
},
{
Network: network.Network{
ID: "nw-b",
Name: "network-b",
},
},
{
Network: network.Network{
ID: "nw-a",
Name: "network-a",
},
},
{ID: "nw-c", Name: "network-c"},
{ID: "nw-b", Name: "network-b"},
{ID: "nw-a", Name: "network-a"},
},
expOut: []string{"network-c", "network-b", "network-a"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
@ -286,11 +273,11 @@ func TestCompleteNetworkNames(t *testing.T) {
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
comp := NetworkNames(fakeCLI{&fakeClient{
networkListFunc: func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error) {
networkListFunc: func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
if tc.expDirective == cobra.ShellCompDirectiveError {
return client.NetworkListResult{}, errors.New("some error occurred")
return nil, errors.New("some error occurred")
}
return client.NetworkListResult{Items: tc.networks}, nil
return tc.networks, nil
},
}})
@ -302,7 +289,7 @@ func TestCompleteNetworkNames(t *testing.T) {
}
func TestCompletePlatforms(t *testing.T) {
values, directives := Platforms()(nil, nil, "")
values, directives := Platforms(nil, nil, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
assert.Check(t, is.DeepEqual(values, commonPlatforms))
}
@ -310,7 +297,7 @@ func TestCompletePlatforms(t *testing.T) {
func TestCompleteVolumeNames(t *testing.T) {
tests := []struct {
doc string
volumes []volume.Volume
volumes []*volume.Volume
expOut []string
expDirective cobra.ShellCompDirective
}{
@ -320,7 +307,7 @@ func TestCompleteVolumeNames(t *testing.T) {
},
{
doc: "with results",
volumes: []volume.Volume{
volumes: []*volume.Volume{
{Name: "volume-c"},
{Name: "volume-b"},
{Name: "volume-a"},
@ -337,11 +324,11 @@ func TestCompleteVolumeNames(t *testing.T) {
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
comp := VolumeNames(fakeCLI{&fakeClient{
volumeListFunc: func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error) {
volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) {
if tc.expDirective == cobra.ShellCompDirectiveError {
return client.VolumeListResult{}, errors.New("some error occurred")
return volume.ListResponse{}, errors.New("some error occurred")
}
return client.VolumeListResult{Items: tc.volumes}, nil
return volume.ListResponse{Volumes: tc.volumes}, nil
},
}})

View File

@ -3,41 +3,42 @@ package config
import (
"context"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
)
type fakeClient struct {
client.Client
configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error)
configInspectFunc func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error)
configListFunc func(context.Context, client.ConfigListOptions) (client.ConfigListResult, error)
configRemoveFunc func(context.Context, string, client.ConfigRemoveOptions) (client.ConfigRemoveResult, error)
configCreateFunc func(context.Context, swarm.ConfigSpec) (swarm.ConfigCreateResponse, error)
configInspectFunc func(context.Context, string) (swarm.Config, []byte, error)
configListFunc func(context.Context, swarm.ConfigListOptions) ([]swarm.Config, error)
configRemoveFunc func(string) error
}
func (c *fakeClient) ConfigCreate(ctx context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
func (c *fakeClient) ConfigCreate(ctx context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
if c.configCreateFunc != nil {
return c.configCreateFunc(ctx, options)
return c.configCreateFunc(ctx, spec)
}
return client.ConfigCreateResult{}, nil
return swarm.ConfigCreateResponse{}, nil
}
func (c *fakeClient) ConfigInspect(ctx context.Context, id string, options client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
if c.configInspectFunc != nil {
return c.configInspectFunc(ctx, id, options)
return c.configInspectFunc(ctx, id)
}
return client.ConfigInspectResult{}, nil
return swarm.Config{}, nil, nil
}
func (c *fakeClient) ConfigList(ctx context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
func (c *fakeClient) ConfigList(ctx context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
if c.configListFunc != nil {
return c.configListFunc(ctx, options)
}
return client.ConfigListResult{}, nil
return []swarm.Config{}, nil
}
func (c *fakeClient) ConfigRemove(ctx context.Context, name string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
func (c *fakeClient) ConfigRemove(_ context.Context, name string) error {
if c.configRemoveFunc != nil {
return c.configRemoveFunc(ctx, name, options)
return c.configRemoveFunc(name)
}
return client.ConfigRemoveResult{}, nil
return nil
}

View File

@ -4,16 +4,17 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/internal/commands"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/swarm"
"github.com/spf13/cobra"
)
func init() {
commands.Register(newConfigCommand)
// NewConfigCommand returns a cobra command for `config` subcommands
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewConfigCommand(dockerCLI command.Cli) *cobra.Command {
return newConfigCommand(dockerCLI)
}
// newConfigCommand returns a cobra command for `config` subcommands
func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "config",
@ -24,7 +25,6 @@ func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
"version": "1.30",
"swarm": "manager",
},
DisableFlagsInUseLine: true,
}
cmd.AddCommand(
newConfigListCommand(dockerCLI),
@ -38,12 +38,12 @@ func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
// completeNames offers completion for swarm configs
func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().ConfigList(cmd.Context(), client.ConfigListOptions{})
list, err := dockerCLI.Client().ConfigList(cmd.Context(), swarm.ConfigListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
for _, config := range res.Items {
for _, config := range list {
names = append(names, config.ID)
}
return names, cobra.ShellCompDirectiveNoFileComp

View File

@ -2,19 +2,28 @@ package config
import (
"context"
"errors"
"fmt"
"io"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/opts"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/swarm"
"github.com/moby/sys/sequential"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
// CreateOptions specifies some options that are used when creating a config.
//
// Deprecated: this type was for internal use and will be removed in the next release.
type CreateOptions struct {
Name string
TemplateDriver string
File string
Labels opts.ListOpts
}
// createOptions specifies some options that are used when creating a config.
type createOptions struct {
name string
@ -37,24 +46,7 @@ func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command {
createOpts.file = args[1]
return runCreate(cmd.Context(), dockerCLI, createOpts)
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
switch len(args) {
case 0:
// No completion for the first argument, which is the name for
// the new config, but if a non-empty name is given, we return
// it as completion to allow "tab"-ing to the next completion.
return []string{toComplete}, cobra.ShellCompDirectiveNoFileComp
case 1:
// Second argument is either "-" or a file to load.
//
// TODO(thaJeztah): provide completion for "-".
return nil, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveDefault
default:
// Command only accepts two arguments.
return nil, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
}
},
DisableFlagsInUseLine: true,
ValidArgsFunction: cobra.NoFileCompletions,
}
flags := cmd.Flags()
flags.VarP(&createOpts.labels, "label", "l", "Config labels")
@ -64,13 +56,25 @@ func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command {
return cmd
}
// RunConfigCreate creates a config with the given options.
//
// Deprecated: this function was for internal use and will be removed in the next release.
func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateOptions) error {
return runCreate(ctx, dockerCLI, createOptions{
name: options.Name,
templateDriver: options.TemplateDriver,
file: options.File,
labels: options.Labels,
})
}
// runCreate creates a config with the given options.
func runCreate(ctx context.Context, dockerCLI command.Cli, options createOptions) error {
apiClient := dockerCLI.Client()
configData, err := readConfigData(dockerCLI.In(), options.file)
if err != nil {
return fmt.Errorf("error reading content from %q: %v", options.file, err)
return errors.Errorf("Error reading content from %q: %v", options.file, err)
}
spec := swarm.ConfigSpec{
@ -85,9 +89,7 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, options createOptions
Name: options.templateDriver,
}
}
r, err := apiClient.ConfigCreate(ctx, client.ConfigCreateOptions{
Spec: spec,
})
r, err := apiClient.ConfigCreate(ctx, spec)
if err != nil {
return err
}

View File

@ -12,8 +12,7 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/swarm"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
@ -24,7 +23,7 @@ const configDataFile = "config-create-with-name.golden"
func TestConfigCreateErrors(t *testing.T) {
testCases := []struct {
args []string
configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error)
configCreateFunc func(context.Context, swarm.ConfigSpec) (swarm.ConfigCreateResponse, error)
expectedError string
}{
{
@ -37,8 +36,8 @@ func TestConfigCreateErrors(t *testing.T) {
},
{
args: []string{"name", filepath.Join("testdata", configDataFile)},
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
return client.ConfigCreateResult{}, errors.New("error creating config")
configCreateFunc: func(_ context.Context, configSpec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
return swarm.ConfigCreateResponse{}, errors.New("error creating config")
},
expectedError: "error creating config",
},
@ -62,15 +61,15 @@ func TestConfigCreateWithName(t *testing.T) {
const name = "config-with-name"
var actual []byte
cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
if options.Spec.Name != name {
return client.ConfigCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name)
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
if spec.Name != name {
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name)
}
actual = options.Spec.Data
actual = spec.Data
return client.ConfigCreateResult{
ID: "ID-" + options.Spec.Name,
return swarm.ConfigCreateResponse{
ID: "ID-" + spec.Name,
}, nil
},
})
@ -101,13 +100,13 @@ func TestConfigCreateWithLabels(t *testing.T) {
}
cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
if !reflect.DeepEqual(options.Spec, expected) {
return client.ConfigCreateResult{}, fmt.Errorf("expected %+v, got %+v", expected, options.Spec)
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
if !reflect.DeepEqual(spec, expected) {
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected %+v, got %+v", expected, spec)
}
return client.ConfigCreateResult{
ID: "ID-" + options.Spec.Name,
return swarm.ConfigCreateResponse{
ID: "ID-" + spec.Name,
}, nil
},
})
@ -127,17 +126,17 @@ func TestConfigCreateWithTemplatingDriver(t *testing.T) {
const name = "config-with-template-driver"
cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
if options.Spec.Name != name {
return client.ConfigCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name)
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
if spec.Name != name {
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name)
}
if options.Spec.Templating.Name != expectedDriver.Name {
return client.ConfigCreateResult{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, options.Spec.Labels)
if spec.Templating.Name != expectedDriver.Name {
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
}
return client.ConfigCreateResult{
ID: "ID-" + options.Spec.Name,
return swarm.ConfigCreateResponse{
ID: "ID-" + spec.Name,
}, nil
},
})

View File

@ -7,9 +7,8 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/inspect"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/go-units"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
)
const (
@ -30,6 +29,13 @@ Data:
{{.Data}}`
)
// NewFormat returns a Format for rendering using a config Context
//
// Deprecated: this function was only used internally and will be removed in the next release.
func NewFormat(source string, quiet bool) formatter.Format {
return newFormat(source, quiet)
}
// newFormat returns a Format for rendering using a configContext.
func newFormat(source string, quiet bool) formatter.Format {
switch source {
@ -44,28 +50,38 @@ func newFormat(source string, quiet bool) formatter.Format {
return formatter.Format(source)
}
// FormatWrite writes the context
//
// Deprecated: this function was only used internally and will be removed in the next release.
func FormatWrite(fmtCtx formatter.Context, configs []swarm.Config) error {
return formatWrite(fmtCtx, configs)
}
// formatWrite writes the context
func formatWrite(fmtCtx formatter.Context, configs client.ConfigListResult) error {
cCtx := &configContext{
HeaderContext: formatter.HeaderContext{
Header: formatter.SubHeaderContext{
"ID": configIDHeader,
"Name": formatter.NameHeader,
"CreatedAt": configCreatedHeader,
"UpdatedAt": configUpdatedHeader,
"Labels": formatter.LabelsHeader,
},
},
}
return fmtCtx.Write(cCtx, func(format func(subContext formatter.SubContext) error) error {
for _, config := range configs.Items {
func formatWrite(fmtCtx formatter.Context, configs []swarm.Config) error {
render := func(format func(subContext formatter.SubContext) error) error {
for _, config := range configs {
configCtx := &configContext{c: config}
if err := format(configCtx); err != nil {
return err
}
}
return nil
})
}
return fmtCtx.Write(newConfigContext(), render)
}
func newConfigContext() *configContext {
cCtx := &configContext{}
cCtx.Header = formatter.SubHeaderContext{
"ID": configIDHeader,
"Name": formatter.NameHeader,
"CreatedAt": configCreatedHeader,
"UpdatedAt": configUpdatedHeader,
"Labels": formatter.LabelsHeader,
}
return cCtx
}
type configContext struct {
@ -112,12 +128,19 @@ func (c *configContext) Label(name string) string {
return c.c.Spec.Annotations.Labels[name]
}
// InspectFormatWrite renders the context for a list of configs
//
// Deprecated: this function was only used internally and will be removed in the next release.
func InspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error {
return inspectFormatWrite(fmtCtx, refs, getRef)
}
// inspectFormatWrite renders the context for a list of configs
func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error {
if fmtCtx.Format != configInspectPrettyTemplate {
return inspect.Inspect(fmtCtx.Output, refs, string(fmtCtx.Format), getRef)
}
return fmtCtx.Write(&configInspectContext{}, func(format func(subContext formatter.SubContext) error) error {
render := func(format func(subContext formatter.SubContext) error) error {
for _, ref := range refs {
configI, _, err := getRef(ref)
if err != nil {
@ -132,7 +155,8 @@ func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.
}
}
return nil
})
}
return fmtCtx.Write(&configInspectContext{}, render)
}
type configInspectContext struct {

View File

@ -6,8 +6,7 @@ import (
"time"
"github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/swarm"
"gotest.tools/v3/assert"
)
@ -49,25 +48,23 @@ id_rsa
},
}
res := client.ConfigListResult{
Items: []swarm.Config{
{
ID: "1",
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}},
},
{
ID: "2",
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}},
},
configs := []swarm.Config{
{
ID: "1",
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}},
},
{
ID: "2",
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}},
},
}
for _, tc := range cases {
t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer
tc.context.Output = &out
if err := formatWrite(tc.context, res); err != nil {
if err := formatWrite(tc.context, configs); err != nil {
assert.ErrorContains(t, err, tc.expected)
} else {
assert.Equal(t, out.String(), tc.expected)

View File

@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.23
package config
@ -12,10 +12,18 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
flagsHelper "github.com/docker/cli/cli/flags"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
// InspectOptions contains options for the docker config inspect command.
//
// Deprecated: this type was for internal use and will be removed in the next release.
type InspectOptions struct {
Names []string
Format string
Pretty bool
}
// inspectOptions contains options for the docker config inspect command.
type inspectOptions struct {
names []string
@ -33,8 +41,9 @@ func newConfigInspectCommand(dockerCLI command.Cli) *cobra.Command {
opts.names = args
return runInspect(cmd.Context(), dockerCLI, opts)
},
ValidArgsFunction: completeNames(dockerCLI),
DisableFlagsInUseLine: true,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completeNames(dockerCLI)(cmd, args, toComplete)
},
}
cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp)
@ -42,6 +51,17 @@ func newConfigInspectCommand(dockerCLI command.Cli) *cobra.Command {
return cmd
}
// RunConfigInspect inspects the given Swarm config.
//
// Deprecated: this function was for internal use and will be removed in the next release.
func RunConfigInspect(ctx context.Context, dockerCLI command.Cli, opts InspectOptions) error {
return runInspect(ctx, dockerCLI, inspectOptions{
names: opts.Names,
format: opts.Format,
pretty: opts.Pretty,
})
}
// runInspect inspects the given Swarm config.
func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error {
apiClient := dockerCLI.Client()
@ -51,8 +71,7 @@ func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions)
}
getRef := func(id string) (any, []byte, error) {
res, err := apiClient.ConfigInspect(ctx, id, client.ConfigInspectOptions{})
return res.Config, res.Raw, err
return apiClient.ConfigInspectWithRaw(ctx, id)
}
// check if the user is trying to apply a template to the pretty format, which

View File

@ -10,7 +10,7 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/builders"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/swarm"
"gotest.tools/v3/assert"
"gotest.tools/v3/golden"
)
@ -19,7 +19,7 @@ func TestConfigInspectErrors(t *testing.T) {
testCases := []struct {
args []string
flags map[string]string
configInspectFunc func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
configInspectFunc func(_ context.Context, configID string) (swarm.Config, []byte, error)
expectedError string
}{
{
@ -27,8 +27,8 @@ func TestConfigInspectErrors(t *testing.T) {
},
{
args: []string{"foo"},
configInspectFunc: func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
return client.ConfigInspectResult{}, errors.New("error while inspecting the config")
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
return swarm.Config{}, nil, errors.New("error while inspecting the config")
},
expectedError: "error while inspecting the config",
},
@ -41,13 +41,11 @@ func TestConfigInspectErrors(t *testing.T) {
},
{
args: []string{"foo", "bar"},
configInspectFunc: func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
if configID == "foo" {
return client.ConfigInspectResult{
Config: *builders.Config(builders.ConfigName("foo")),
}, nil
return *builders.Config(builders.ConfigName("foo")), nil, nil
}
return client.ConfigInspectResult{}, errors.New("error while inspecting the config")
return swarm.Config{}, nil, errors.New("error while inspecting the config")
},
expectedError: "error while inspecting the config",
},
@ -72,34 +70,25 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
testCases := []struct {
name string
args []string
configInspectFunc func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
configInspectFunc func(_ context.Context, configID string) (swarm.Config, []byte, error)
}{
{
name: "single-config",
args: []string{"foo"},
configInspectFunc: func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
if name != "foo" {
return client.ConfigInspectResult{}, fmt.Errorf("invalid name, expected %s, got %s", "foo", name)
return swarm.Config{}, nil, fmt.Errorf("invalid name, expected %s, got %s", "foo", name)
}
return client.ConfigInspectResult{
Config: *builders.Config(
builders.ConfigID("ID-foo"),
builders.ConfigName("foo"),
),
}, nil
return *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), nil, nil
},
},
{
name: "multiple-configs-with-labels",
args: []string{"foo", "bar"},
configInspectFunc: func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
return client.ConfigInspectResult{
Config: *builders.Config(
builders.ConfigID("ID-"+name),
builders.ConfigName(name),
builders.ConfigLabels(map[string]string{"label1": "label-foo"}),
),
}, nil
configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
return *builders.Config(builders.ConfigID("ID-"+name), builders.ConfigName(name), builders.ConfigLabels(map[string]string{
"label1": "label-foo",
})), nil, nil
},
},
}
@ -113,19 +102,16 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
}
func TestConfigInspectWithFormat(t *testing.T) {
configInspectFunc := func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
return client.ConfigInspectResult{
Config: *builders.Config(
builders.ConfigName("foo"),
builders.ConfigLabels(map[string]string{"label1": "label-foo"}),
),
}, nil
configInspectFunc := func(_ context.Context, name string) (swarm.Config, []byte, error) {
return *builders.Config(builders.ConfigName("foo"), builders.ConfigLabels(map[string]string{
"label1": "label-foo",
})), nil, nil
}
testCases := []struct {
name string
format string
args []string
configInspectFunc func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
configInspectFunc func(_ context.Context, name string) (swarm.Config, []byte, error)
}{
{
name: "simple-template",
@ -155,23 +141,21 @@ func TestConfigInspectWithFormat(t *testing.T) {
func TestConfigInspectPretty(t *testing.T) {
testCases := []struct {
name string
configInspectFunc func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error)
configInspectFunc func(context.Context, string) (swarm.Config, []byte, error)
}{
{
name: "simple",
configInspectFunc: func(_ context.Context, id string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
return client.ConfigInspectResult{
Config: *builders.Config(
builders.ConfigLabels(map[string]string{
"lbl1": "value1",
}),
builders.ConfigID("configID"),
builders.ConfigName("configName"),
builders.ConfigCreatedAt(time.Time{}),
builders.ConfigUpdatedAt(time.Time{}),
builders.ConfigData([]byte("payload here")),
),
}, nil
configInspectFunc: func(_ context.Context, id string) (swarm.Config, []byte, error) {
return *builders.Config(
builders.ConfigLabels(map[string]string{
"lbl1": "value1",
}),
builders.ConfigID("configID"),
builders.ConfigName("configName"),
builders.ConfigCreatedAt(time.Time{}),
builders.ConfigUpdatedAt(time.Time{}),
builders.ConfigData([]byte("payload here")),
), []byte{}, nil
},
},
}

View File

@ -9,11 +9,20 @@ import (
"github.com/docker/cli/cli/command/formatter"
flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types/swarm"
"github.com/fvbommel/sortorder"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
// ListOptions contains options for the docker config ls command.
//
// Deprecated: this type was for internal use and will be removed in the next release.
type ListOptions struct {
Quiet bool
Format string
Filter opts.FilterOpt
}
// listOptions contains options for the docker config ls command.
type listOptions struct {
quiet bool
@ -32,8 +41,7 @@ func newConfigListCommand(dockerCLI command.Cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
return runList(cmd.Context(), dockerCLI, listOpts)
},
ValidArgsFunction: cobra.NoFileCompletions,
DisableFlagsInUseLine: true,
ValidArgsFunction: cobra.NoFileCompletions,
}
flags := cmd.Flags()
@ -44,11 +52,22 @@ func newConfigListCommand(dockerCLI command.Cli) *cobra.Command {
return cmd
}
// RunConfigList lists Swarm configs.
//
// Deprecated: this function was for internal use and will be removed in the next release.
func RunConfigList(ctx context.Context, dockerCLI command.Cli, options ListOptions) error {
return runList(ctx, dockerCLI, listOptions{
quiet: options.Quiet,
format: options.Format,
filter: options.Filter,
})
}
// runList lists Swarm configs.
func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error {
apiClient := dockerCLI.Client()
res, err := apiClient.ConfigList(ctx, client.ConfigListOptions{Filters: options.filter.Value()})
configs, err := apiClient.ConfigList(ctx, swarm.ConfigListOptions{Filters: options.filter.Value()})
if err != nil {
return err
}
@ -62,13 +81,13 @@ func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) er
}
}
sort.Slice(res.Items, func(i, j int) bool {
return sortorder.NaturalLess(res.Items[i].Spec.Name, res.Items[j].Spec.Name)
sort.Slice(configs, func(i, j int) bool {
return sortorder.NaturalLess(configs[i].Spec.Name, configs[j].Spec.Name)
})
configCtx := formatter.Context{
Output: dockerCLI.Out(),
Format: newFormat(format, options.quiet),
}
return formatWrite(configCtx, res)
return formatWrite(configCtx, configs)
}

View File

@ -10,16 +10,16 @@ import (
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/builders"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/swarm"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
)
func TestConfigListErrors(t *testing.T) {
testCases := []struct {
args []string
configListFunc func(context.Context, client.ConfigListOptions) (client.ConfigListResult, error)
configListFunc func(context.Context, swarm.ConfigListOptions) ([]swarm.Config, error)
expectedError string
}{
{
@ -27,8 +27,8 @@ func TestConfigListErrors(t *testing.T) {
expectedError: "accepts no argument",
},
{
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
return client.ConfigListResult{}, errors.New("error listing configs")
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{}, errors.New("error listing configs")
},
expectedError: "error listing configs",
},
@ -48,28 +48,26 @@ func TestConfigListErrors(t *testing.T) {
func TestConfigList(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
return client.ConfigListResult{
Items: []swarm.Config{
*builders.Config(builders.ConfigID("ID-1-foo"),
builders.ConfigName("1-foo"),
builders.ConfigVersion(swarm.Version{Index: 10}),
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
*builders.Config(builders.ConfigID("ID-10-foo"),
builders.ConfigName("10-foo"),
builders.ConfigVersion(swarm.Version{Index: 11}),
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
*builders.Config(builders.ConfigID("ID-2-foo"),
builders.ConfigName("2-foo"),
builders.ConfigVersion(swarm.Version{Index: 11}),
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
},
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{
*builders.Config(builders.ConfigID("ID-1-foo"),
builders.ConfigName("1-foo"),
builders.ConfigVersion(swarm.Version{Index: 10}),
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
*builders.Config(builders.ConfigID("ID-10-foo"),
builders.ConfigName("10-foo"),
builders.ConfigVersion(swarm.Version{Index: 11}),
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
*builders.Config(builders.ConfigID("ID-2-foo"),
builders.ConfigName("2-foo"),
builders.ConfigVersion(swarm.Version{Index: 11}),
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
}, nil
},
})
@ -80,14 +78,12 @@ func TestConfigList(t *testing.T) {
func TestConfigListWithQuietOption(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
return client.ConfigListResult{
Items: []swarm.Config{
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
"label": "label-bar",
})),
},
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
"label": "label-bar",
})),
}, nil
},
})
@ -99,14 +95,12 @@ func TestConfigListWithQuietOption(t *testing.T) {
func TestConfigListWithConfigFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
return client.ConfigListResult{
Items: []swarm.Config{
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
"label": "label-bar",
})),
},
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
"label": "label-bar",
})),
}, nil
},
})
@ -120,14 +114,12 @@ func TestConfigListWithConfigFormat(t *testing.T) {
func TestConfigListWithFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
return client.ConfigListResult{
Items: []swarm.Config{
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
"label": "label-bar",
})),
},
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
"label": "label-bar",
})),
}, nil
},
})
@ -139,24 +131,22 @@ func TestConfigListWithFormat(t *testing.T) {
func TestConfigListWithFilter(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
assert.Check(t, options.Filters["name"]["foo"])
assert.Check(t, options.Filters["label"]["lbl1=Label-bar"])
return client.ConfigListResult{
Items: []swarm.Config{
*builders.Config(builders.ConfigID("ID-foo"),
builders.ConfigName("foo"),
builders.ConfigVersion(swarm.Version{Index: 10}),
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
*builders.Config(builders.ConfigID("ID-bar"),
builders.ConfigName("bar"),
builders.ConfigVersion(swarm.Version{Index: 11}),
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
},
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
assert.Check(t, is.Equal("foo", options.Filters.Get("name")[0]))
assert.Check(t, is.Equal("lbl1=Label-bar", options.Filters.Get("label")[0]))
return []swarm.Config{
*builders.Config(builders.ConfigID("ID-foo"),
builders.ConfigName("foo"),
builders.ConfigVersion(swarm.Version{Index: 10}),
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
*builders.Config(builders.ConfigID("ID-bar"),
builders.ConfigName("bar"),
builders.ConfigVersion(swarm.Version{Index: 11}),
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
}, nil
},
})

View File

@ -7,10 +7,16 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
// RemoveOptions contains options for the docker config rm command.
//
// Deprecated: this type was for internal use and will be removed in the next release.
type RemoveOptions struct {
Names []string
}
func newConfigRemoveCommand(dockerCLI command.Cli) *cobra.Command {
return &cobra.Command{
Use: "rm CONFIG [CONFIG...]",
@ -20,18 +26,26 @@ func newConfigRemoveCommand(dockerCLI command.Cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
return runRemove(cmd.Context(), dockerCLI, args)
},
ValidArgsFunction: completeNames(dockerCLI),
DisableFlagsInUseLine: true,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completeNames(dockerCLI)(cmd, args, toComplete)
},
}
}
// RunConfigRemove removes the given Swarm configs.
//
// Deprecated: this function was for internal use and will be removed in the next release.
func RunConfigRemove(ctx context.Context, dockerCLI command.Cli, opts RemoveOptions) error {
return runRemove(ctx, dockerCLI, opts.Names)
}
// runRemove removes the given Swarm configs.
func runRemove(ctx context.Context, dockerCLI command.Cli, names []string) error {
apiClient := dockerCLI.Client()
var errs []error
for _, name := range names {
if _, err := apiClient.ConfigRemove(ctx, name, client.ConfigRemoveOptions{}); err != nil {
if err := apiClient.ConfigRemove(ctx, name); err != nil {
errs = append(errs, err)
continue
}

View File

@ -1,14 +1,12 @@
package config
import (
"context"
"errors"
"io"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
@ -16,7 +14,7 @@ import (
func TestConfigRemoveErrors(t *testing.T) {
testCases := []struct {
args []string
configRemoveFunc func(context.Context, string, client.ConfigRemoveOptions) (client.ConfigRemoveResult, error)
configRemoveFunc func(string) error
expectedError string
}{
{
@ -25,8 +23,8 @@ func TestConfigRemoveErrors(t *testing.T) {
},
{
args: []string{"foo"},
configRemoveFunc: func(ctx context.Context, name string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
return client.ConfigRemoveResult{}, errors.New("error removing config")
configRemoveFunc: func(name string) error {
return errors.New("error removing config")
},
expectedError: "error removing config",
},
@ -48,9 +46,9 @@ func TestConfigRemoveWithName(t *testing.T) {
names := []string{"foo", "bar"}
var removedConfigs []string
cli := test.NewFakeCli(&fakeClient{
configRemoveFunc: func(_ context.Context, name string, _ client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
configRemoveFunc: func(name string) error {
removedConfigs = append(removedConfigs, name)
return client.ConfigRemoveResult{}, nil
return nil
},
})
cmd := newConfigRemoveCommand(cli)
@ -65,12 +63,12 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
var removedConfigs []string
cli := test.NewFakeCli(&fakeClient{
configRemoveFunc: func(_ context.Context, name string, _ client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
configRemoveFunc: func(name string) error {
removedConfigs = append(removedConfigs, name)
if name == "foo" {
return client.ConfigRemoveResult{}, errors.New("error removing config: " + name)
return errors.New("error removing config: " + name)
}
return client.ConfigRemoveResult{}, nil
return nil
},
})

View File

@ -2,15 +2,15 @@ package container
import (
"context"
"errors"
"io"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/moby/sys/signal"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@ -23,24 +23,30 @@ type AttachOptions struct {
}
func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClient, args string) (*container.InspectResponse, error) {
c, err := apiClient.ContainerInspect(ctx, args, client.ContainerInspectOptions{})
c, err := apiClient.ContainerInspect(ctx, args)
if err != nil {
return nil, err
}
if !c.Container.State.Running {
return nil, errors.New("cannot attach to a stopped container, start it first")
if !c.State.Running {
return nil, errors.New("You cannot attach to a stopped container, start it first")
}
if c.Container.State.Paused {
return nil, errors.New("cannot attach to a paused container, unpause it first")
if c.State.Paused {
return nil, errors.New("You cannot attach to a paused container, unpause it first")
}
if c.Container.State.Restarting {
return nil, errors.New("cannot attach to a restarting container, wait until it is running")
if c.State.Restarting {
return nil, errors.New("You cannot attach to a restarting container, wait until it is running")
}
return &c.Container, nil
return &c, nil
}
// NewAttachCommand creates a new cobra.Command for `docker attach`
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewAttachCommand(dockerCLI command.Cli) *cobra.Command {
return newAttachCommand(dockerCLI)
}
// newAttachCommand creates a new cobra.Command for `docker attach`
func newAttachCommand(dockerCLI command.Cli) *cobra.Command {
var opts AttachOptions
@ -58,7 +64,6 @@ func newAttachCommand(dockerCLI command.Cli) *cobra.Command {
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool {
return ctr.State != container.StatePaused
}),
DisableFlagsInUseLine: true,
}
flags := cmd.Flags()
@ -74,7 +79,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
// request channel to wait for client
waitCtx := context.WithoutCancel(ctx)
waitRes := apiClient.ContainerWait(waitCtx, containerID, client.ContainerWaitOptions{})
resultC, errC := apiClient.ContainerWait(waitCtx, containerID, "")
c, err := inspectContainerAndCheckState(ctx, apiClient, containerID)
if err != nil {
@ -90,7 +95,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
detachKeys = opts.DetachKeys
}
options := client.ContainerAttachOptions{
options := container.AttachOptions{
Stream: true,
Stdin: !opts.NoStdin && c.Config.OpenStdin,
Stdout: true,
@ -114,11 +119,11 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
defer signal.StopCatch(sigc)
}
res, err := apiClient.ContainerAttach(ctx, containerID, options)
if err != nil {
return err
resp, errAttach := apiClient.ContainerAttach(ctx, containerID, options)
if errAttach != nil {
return errAttach
}
defer res.HijackedResponse.Close()
defer resp.Close()
// If use docker attach command to attach to a stop container, it will return
// "You cannot attach to a stopped container" error, it's ok, but when
@ -142,7 +147,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
inputStream: in,
outputStream: dockerCLI.Out(),
errorStream: dockerCLI.Err(),
resp: res.HijackedResponse,
resp: resp,
tty: c.Config.Tty,
detachKeys: options.DetachKeys,
}
@ -152,19 +157,19 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
return err
}
return getExitStatus(waitRes)
return getExitStatus(errC, resultC)
}
func getExitStatus(waitRes client.ContainerWaitResult) error {
func getExitStatus(errC <-chan error, resultC <-chan container.WaitResponse) error {
select {
case result := <-waitRes.Result:
case result := <-resultC:
if result.Error != nil {
return errors.New(result.Error.Message)
}
if result.StatusCode != 0 {
return cli.StatusError{StatusCode: int(result.StatusCode)}
}
case err := <-waitRes.Error:
case err := <-errC:
return err
}
@ -177,7 +182,7 @@ func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
// resize it, then go back to normal. Without this, every attach after the first will
// require the user to manually resize or hit enter.
resizeTTYTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
resizeTtyTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
// to the actual size.

View File

@ -7,8 +7,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"gotest.tools/v3/assert"
)
@ -17,23 +16,23 @@ func TestNewAttachCommandErrors(t *testing.T) {
name string
args []string
expectedError string
containerInspectFunc func(img string) (client.ContainerInspectResult, error)
containerInspectFunc func(img string) (container.InspectResponse, error)
}{
{
name: "client-error",
args: []string{"5cb5bb5e4a3b"},
expectedError: "something went wrong",
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
return client.ContainerInspectResult{}, errors.New("something went wrong")
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
return container.InspectResponse{}, errors.New("something went wrong")
},
},
{
name: "client-stopped",
args: []string{"5cb5bb5e4a3b"},
expectedError: "cannot attach to a stopped container",
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
return client.ContainerInspectResult{
Container: container.InspectResponse{
expectedError: "You cannot attach to a stopped container",
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
return container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
State: &container.State{
Running: false,
},
@ -44,10 +43,10 @@ func TestNewAttachCommandErrors(t *testing.T) {
{
name: "client-paused",
args: []string{"5cb5bb5e4a3b"},
expectedError: "cannot attach to a paused container",
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
return client.ContainerInspectResult{
Container: container.InspectResponse{
expectedError: "You cannot attach to a paused container",
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
return container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
State: &container.State{
Running: true,
Paused: true,
@ -59,10 +58,10 @@ func TestNewAttachCommandErrors(t *testing.T) {
{
name: "client-restarting",
args: []string{"5cb5bb5e4a3b"},
expectedError: "cannot attach to a restarting container",
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
return client.ContainerInspectResult{
Container: container.InspectResponse{
expectedError: "You cannot attach to a restarting container",
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
return container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
State: &container.State{
Running: true,
Paused: false,
@ -125,10 +124,7 @@ func TestGetExitStatus(t *testing.T) {
resultC <- *testcase.result
}
err := getExitStatus(client.ContainerWaitResult{
Result: resultC,
Error: errC,
})
err := getExitStatus(errC, resultC)
if testcase.expectedError == nil {
assert.NilError(t, err)

View File

@ -3,232 +3,232 @@ package container
import (
"context"
"io"
"net/http"
"strings"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/client"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
func mockContainerExportResult(content string) client.ContainerExportResult {
return io.NopCloser(strings.NewReader(content))
}
func mockContainerLogsResult(content string) client.ContainerLogsResult {
return io.NopCloser(strings.NewReader(content))
}
type fakeStreamResult struct {
io.ReadCloser
client.ImagePushResponse // same interface as [client.ImagePushResponse]
}
func (e fakeStreamResult) Read(p []byte) (int, error) { return e.ReadCloser.Read(p) }
func (e fakeStreamResult) Close() error { return e.ReadCloser.Close() }
type fakeClient struct {
client.Client
inspectFunc func(string) (client.ContainerInspectResult, error)
execInspectFunc func(execID string) (client.ExecInspectResult, error)
execCreateFunc func(containerID string, options client.ExecCreateOptions) (client.ExecCreateResult, error)
createContainerFunc func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error)
containerStartFunc func(containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error)
imagePullFunc func(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error)
infoFunc func() (client.SystemInfoResult, error)
containerStatPathFunc func(containerID, path string) (client.ContainerStatPathResult, error)
containerCopyFromFunc func(containerID, srcPath string) (client.CopyFromContainerResult, error)
logFunc func(string, client.ContainerLogsOptions) (client.ContainerLogsResult, error)
waitFunc func(string) client.ContainerWaitResult
containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error)
containerExportFunc func(string) (client.ContainerExportResult, error)
containerExecResizeFunc func(id string, options client.ExecResizeOptions) (client.ExecResizeResult, error)
containerRemoveFunc func(ctx context.Context, containerID string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error)
containerRestartFunc func(ctx context.Context, containerID string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error)
containerStopFunc func(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error)
containerKillFunc func(ctx context.Context, containerID string, options client.ContainerKillOptions) (client.ContainerKillResult, error)
containerPruneFunc func(ctx context.Context, options client.ContainerPruneOptions) (client.ContainerPruneResult, error)
containerAttachFunc func(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error)
containerDiffFunc func(ctx context.Context, containerID string) (client.ContainerDiffResult, error)
inspectFunc func(string) (container.InspectResponse, error)
execInspectFunc func(execID string) (container.ExecInspect, error)
execCreateFunc func(containerID string, options container.ExecOptions) (container.ExecCreateResponse, error)
createContainerFunc func(config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
platform *ocispec.Platform,
containerName string) (container.CreateResponse, error)
containerStartFunc func(containerID string, options container.StartOptions) error
imageCreateFunc func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error)
infoFunc func() (system.Info, error)
containerStatPathFunc func(containerID, path string) (container.PathStat, error)
containerCopyFromFunc func(containerID, srcPath string) (io.ReadCloser, container.PathStat, error)
logFunc func(string, container.LogsOptions) (io.ReadCloser, error)
waitFunc func(string) (<-chan container.WaitResponse, <-chan error)
containerListFunc func(container.ListOptions) ([]container.Summary, error)
containerExportFunc func(string) (io.ReadCloser, error)
containerExecResizeFunc func(id string, options container.ResizeOptions) error
containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error
containerRestartFunc func(ctx context.Context, containerID string, options container.StopOptions) error
containerStopFunc func(ctx context.Context, containerID string, options container.StopOptions) error
containerKillFunc func(ctx context.Context, containerID, signal string) error
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error)
containerAttachFunc func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error)
containerDiffFunc func(ctx context.Context, containerID string) ([]container.FilesystemChange, error)
containerRenameFunc func(ctx context.Context, oldName, newName string) error
containerCommitFunc func(ctx context.Context, container string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error)
containerPauseFunc func(ctx context.Context, container string, options client.ContainerPauseOptions) (client.ContainerPauseResult, error)
containerCommitFunc func(ctx context.Context, container string, options container.CommitOptions) (container.CommitResponse, error)
containerPauseFunc func(ctx context.Context, container string) error
Version string
}
func (f *fakeClient) ContainerList(_ context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) {
func (f *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]container.Summary, error) {
if f.containerListFunc != nil {
return f.containerListFunc(options)
}
return client.ContainerListResult{}, nil
return []container.Summary{}, nil
}
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string, _ client.ContainerInspectOptions) (client.ContainerInspectResult, error) {
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (container.InspectResponse, error) {
if f.inspectFunc != nil {
return f.inspectFunc(containerID)
}
return client.ContainerInspectResult{}, nil
return container.InspectResponse{}, nil
}
func (f *fakeClient) ExecCreate(_ context.Context, containerID string, config client.ExecCreateOptions) (client.ExecCreateResult, error) {
func (f *fakeClient) ContainerExecCreate(_ context.Context, containerID string, config container.ExecOptions) (container.ExecCreateResponse, error) {
if f.execCreateFunc != nil {
return f.execCreateFunc(containerID, config)
}
return client.ExecCreateResult{}, nil
return container.ExecCreateResponse{}, nil
}
func (f *fakeClient) ExecInspect(_ context.Context, execID string, _ client.ExecInspectOptions) (client.ExecInspectResult, error) {
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (container.ExecInspect, error) {
if f.execInspectFunc != nil {
return f.execInspectFunc(execID)
}
return client.ExecInspectResult{}, nil
return container.ExecInspect{}, nil
}
func (*fakeClient) ExecStart(context.Context, string, client.ExecStartOptions) (client.ExecStartResult, error) {
return client.ExecStartResult{}, nil
func (*fakeClient) ContainerExecStart(context.Context, string, container.ExecStartOptions) error {
return nil
}
func (f *fakeClient) ContainerCreate(_ context.Context, options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
func (f *fakeClient) ContainerCreate(
_ context.Context,
config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
platform *ocispec.Platform,
containerName string,
) (container.CreateResponse, error) {
if f.createContainerFunc != nil {
return f.createContainerFunc(options)
return f.createContainerFunc(config, hostConfig, networkingConfig, platform, containerName)
}
return client.ContainerCreateResult{}, nil
return container.CreateResponse{}, nil
}
func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error) {
func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, options container.RemoveOptions) error {
if f.containerRemoveFunc != nil {
return f.containerRemoveFunc(ctx, containerID, options)
}
return client.ContainerRemoveResult{}, nil
return nil
}
func (f *fakeClient) ImagePull(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
if f.imagePullFunc != nil {
return f.imagePullFunc(ctx, parentReference, options)
func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
if f.imageCreateFunc != nil {
return f.imageCreateFunc(ctx, parentReference, options)
}
return fakeStreamResult{}, nil
return nil, nil
}
func (f *fakeClient) Info(context.Context, client.InfoOptions) (client.SystemInfoResult, error) {
func (f *fakeClient) Info(_ context.Context) (system.Info, error) {
if f.infoFunc != nil {
return f.infoFunc()
}
return client.SystemInfoResult{}, nil
return system.Info{}, nil
}
func (f *fakeClient) ContainerStatPath(_ context.Context, containerID string, options client.ContainerStatPathOptions) (client.ContainerStatPathResult, error) {
func (f *fakeClient) ContainerStatPath(_ context.Context, containerID, path string) (container.PathStat, error) {
if f.containerStatPathFunc != nil {
return f.containerStatPathFunc(containerID, options.Path)
return f.containerStatPathFunc(containerID, path)
}
return client.ContainerStatPathResult{}, nil
return container.PathStat{}, nil
}
func (f *fakeClient) CopyFromContainer(_ context.Context, containerID string, options client.CopyFromContainerOptions) (client.CopyFromContainerResult, error) {
func (f *fakeClient) CopyFromContainer(_ context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
if f.containerCopyFromFunc != nil {
return f.containerCopyFromFunc(containerID, options.SourcePath)
return f.containerCopyFromFunc(containerID, srcPath)
}
return client.CopyFromContainerResult{}, nil
return nil, container.PathStat{}, nil
}
func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options client.ContainerLogsOptions) (client.ContainerLogsResult, error) {
func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
if f.logFunc != nil {
return f.logFunc(containerID, options)
}
return http.NoBody, nil
return nil, nil
}
func (f *fakeClient) ClientVersion() string {
return f.Version
}
func (f *fakeClient) ContainerWait(_ context.Context, containerID string, _ client.ContainerWaitOptions) client.ContainerWaitResult {
func (f *fakeClient) ContainerWait(_ context.Context, containerID string, _ container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
if f.waitFunc != nil {
return f.waitFunc(containerID)
}
return client.ContainerWaitResult{}
return nil, nil
}
func (f *fakeClient) ContainerStart(_ context.Context, containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error) {
func (f *fakeClient) ContainerStart(_ context.Context, containerID string, options container.StartOptions) error {
if f.containerStartFunc != nil {
return f.containerStartFunc(containerID, options)
}
return client.ContainerStartResult{}, nil
return nil
}
func (f *fakeClient) ContainerExport(_ context.Context, containerID string, _ client.ContainerExportOptions) (client.ContainerExportResult, error) {
func (f *fakeClient) ContainerExport(_ context.Context, containerID string) (io.ReadCloser, error) {
if f.containerExportFunc != nil {
return f.containerExportFunc(containerID)
}
return http.NoBody, nil
return nil, nil
}
func (f *fakeClient) ExecResize(_ context.Context, id string, options client.ExecResizeOptions) (client.ExecResizeResult, error) {
func (f *fakeClient) ContainerExecResize(_ context.Context, id string, options container.ResizeOptions) error {
if f.containerExecResizeFunc != nil {
return f.containerExecResizeFunc(id, options)
}
return client.ExecResizeResult{}, nil
return nil
}
func (f *fakeClient) ContainerKill(ctx context.Context, containerID string, options client.ContainerKillOptions) (client.ContainerKillResult, error) {
func (f *fakeClient) ContainerKill(ctx context.Context, containerID, signal string) error {
if f.containerKillFunc != nil {
return f.containerKillFunc(ctx, containerID, options)
return f.containerKillFunc(ctx, containerID, signal)
}
return client.ContainerKillResult{}, nil
return nil
}
func (f *fakeClient) ContainerPrune(ctx context.Context, options client.ContainerPruneOptions) (client.ContainerPruneResult, error) {
func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
if f.containerPruneFunc != nil {
return f.containerPruneFunc(ctx, options)
return f.containerPruneFunc(ctx, pruneFilters)
}
return client.ContainerPruneResult{}, nil
return container.PruneReport{}, nil
}
func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error) {
func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error {
if f.containerRestartFunc != nil {
return f.containerRestartFunc(ctx, containerID, options)
}
return client.ContainerRestartResult{}, nil
return nil
}
func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error) {
func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error {
if f.containerStopFunc != nil {
return f.containerStopFunc(ctx, containerID, options)
}
return client.ContainerStopResult{}, nil
return nil
}
func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error) {
func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
if f.containerAttachFunc != nil {
return f.containerAttachFunc(ctx, containerID, options)
}
return client.ContainerAttachResult{}, nil
return types.HijackedResponse{}, nil
}
func (f *fakeClient) ContainerDiff(ctx context.Context, containerID string, _ client.ContainerDiffOptions) (client.ContainerDiffResult, error) {
func (f *fakeClient) ContainerDiff(ctx context.Context, containerID string) ([]container.FilesystemChange, error) {
if f.containerDiffFunc != nil {
return f.containerDiffFunc(ctx, containerID)
}
return client.ContainerDiffResult{}, nil
return []container.FilesystemChange{}, nil
}
func (f *fakeClient) ContainerRename(ctx context.Context, oldName string, options client.ContainerRenameOptions) (client.ContainerRenameResult, error) {
func (f *fakeClient) ContainerRename(ctx context.Context, oldName, newName string) error {
if f.containerRenameFunc != nil {
return client.ContainerRenameResult{}, f.containerRenameFunc(ctx, oldName, options.NewName)
return f.containerRenameFunc(ctx, oldName, newName)
}
return client.ContainerRenameResult{}, nil
return nil
}
func (f *fakeClient) ContainerCommit(ctx context.Context, containerID string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
func (f *fakeClient) ContainerCommit(ctx context.Context, containerID string, options container.CommitOptions) (container.CommitResponse, error) {
if f.containerCommitFunc != nil {
return f.containerCommitFunc(ctx, containerID, options)
}
return client.ContainerCommitResult{}, nil
return container.CommitResponse{}, nil
}
func (f *fakeClient) ContainerPause(ctx context.Context, containerID string, options client.ContainerPauseOptions) (client.ContainerPauseResult, error) {
func (f *fakeClient) ContainerPause(ctx context.Context, containerID string) error {
if f.containerPauseFunc != nil {
return f.containerPauseFunc(ctx, containerID, options)
return f.containerPauseFunc(ctx, containerID)
}
return client.ContainerPauseResult{}, nil
return nil
}

View File

@ -3,46 +3,22 @@ package container
import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/commands"
"github.com/spf13/cobra"
)
func init() {
commands.Register(newRunCommand)
commands.Register(newExecCommand)
commands.Register(newPsCommand)
commands.Register(newContainerCommand)
commands.RegisterLegacy(newAttachCommand)
commands.RegisterLegacy(newCommitCommand)
commands.RegisterLegacy(newCopyCommand)
commands.RegisterLegacy(newCreateCommand)
commands.RegisterLegacy(newDiffCommand)
commands.RegisterLegacy(newExportCommand)
commands.RegisterLegacy(newKillCommand)
commands.RegisterLegacy(newLogsCommand)
commands.RegisterLegacy(newPauseCommand)
commands.RegisterLegacy(newPortCommand)
commands.RegisterLegacy(newRenameCommand)
commands.RegisterLegacy(newRestartCommand)
commands.RegisterLegacy(newRmCommand)
commands.RegisterLegacy(newStartCommand)
commands.RegisterLegacy(newStatsCommand)
commands.RegisterLegacy(newStopCommand)
commands.RegisterLegacy(newTopCommand)
commands.RegisterLegacy(newUnpauseCommand)
commands.RegisterLegacy(newUpdateCommand)
commands.RegisterLegacy(newWaitCommand)
// NewContainerCommand returns a cobra command for `container` subcommands
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewContainerCommand(dockerCLI command.Cli) *cobra.Command {
return newContainerCommand(dockerCLI)
}
// newContainerCommand returns a cobra command for `container` subcommands
func newContainerCommand(dockerCLI command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "container",
Short: "Manage containers",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCLI.Err()),
DisableFlagsInUseLine: true,
}
cmd.AddCommand(
newAttachCommand(dockerCLI),

View File

@ -2,14 +2,13 @@ package container
import (
"context"
"errors"
"fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/opts"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"github.com/spf13/cobra"
)
@ -18,13 +17,18 @@ type commitOptions struct {
reference string
pause bool
noPause bool
comment string
author string
changes opts.ListOpts
}
// newCommitCommand creates a new cobra.Command for `docker commit`
// NewCommitCommand creates a new cobra.Command for `docker commit`
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewCommitCommand(dockerCLI command.Cli) *cobra.Command {
return newCommitCommand(dockerCLI)
}
func newCommitCommand(dockerCLI command.Cli) *cobra.Command {
var options commitOptions
@ -37,29 +41,18 @@ func newCommitCommand(dockerCLI command.Cli) *cobra.Command {
if len(args) > 1 {
options.reference = args[1]
}
if cmd.Flag("pause").Changed {
if cmd.Flag("no-pause").Changed {
return errors.New("conflicting options: --no-pause and --pause cannot be used together")
}
options.noPause = !options.pause
}
return runCommit(cmd.Context(), dockerCLI, &options)
},
Annotations: map[string]string{
"aliases": "docker container commit, docker commit",
},
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
DisableFlagsInUseLine: true,
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
}
flags := cmd.Flags()
flags.SetInterspersed(false)
// TODO(thaJeztah): Deprecated: the --pause flag was deprecated in v29 and can be removed in v30.
flags.BoolVarP(&options.pause, "pause", "p", true, "Pause container during commit (deprecated: use --no-pause instead)")
_ = flags.MarkDeprecated("pause", "and enabled by default. Use --no-pause to disable pausing during commit.")
flags.BoolVar(&options.noPause, "no-pause", false, "Disable pausing container during commit")
flags.BoolVarP(&options.pause, "pause", "p", true, "Pause container during commit")
flags.StringVarP(&options.comment, "message", "m", "", "Commit message")
flags.StringVarP(&options.author, "author", "a", "", `Author (e.g., "John Hannibal Smith <hannibal@a-team.com>")`)
@ -70,17 +63,17 @@ func newCommitCommand(dockerCLI command.Cli) *cobra.Command {
}
func runCommit(ctx context.Context, dockerCli command.Cli, options *commitOptions) error {
response, err := dockerCli.Client().ContainerCommit(ctx, options.container, client.ContainerCommitOptions{
response, err := dockerCli.Client().ContainerCommit(ctx, options.container, container.CommitOptions{
Reference: options.reference,
Comment: options.comment,
Author: options.author,
Changes: options.changes.GetSlice(),
NoPause: options.noPause,
Pause: options.pause,
})
if err != nil {
return err
}
_, _ = fmt.Fprintln(dockerCli.Out(), response.ID)
fmt.Fprintln(dockerCli.Out(), response.ID)
return nil
}

View File

@ -7,21 +7,25 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestRunCommit(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerCommitFunc: func(ctx context.Context, ctr string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
containerCommitFunc: func(
ctx context.Context,
ctr string,
options container.CommitOptions,
) (container.CommitResponse, error) {
assert.Check(t, is.Equal(options.Author, "Author Name <author@name.com>"))
assert.Check(t, is.DeepEqual(options.Changes, []string{"EXPOSE 80"}))
assert.Check(t, is.Equal(options.Comment, "commit message"))
assert.Check(t, is.Equal(options.NoPause, true))
assert.Check(t, is.Equal(options.Pause, false))
assert.Check(t, is.Equal(ctr, "container-id"))
return client.ContainerCommitResult{ID: "image-id"}, nil
return container.CommitResponse{ID: "image-id"}, nil
},
})
@ -32,7 +36,7 @@ func TestRunCommit(t *testing.T) {
"--author", "Author Name <author@name.com>",
"--change", "EXPOSE 80",
"--message", "commit message",
"--no-pause",
"--pause=false",
"container-id",
},
)
@ -47,8 +51,12 @@ func TestRunCommitClientError(t *testing.T) {
clientError := errors.New("client error")
cli := test.NewFakeCli(&fakeClient{
containerCommitFunc: func(ctx context.Context, ctr string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
return client.ContainerCommitResult{}, clientError
containerCommitFunc: func(
ctx context.Context,
ctr string,
options container.CommitOptions,
) (container.CommitResponse, error) {
return container.CommitResponse{}, clientError
},
})

View File

@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.23
package container
@ -8,8 +8,7 @@ import (
"sync"
"github.com/docker/cli/cli/command/completion"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"github.com/moby/sys/capability"
"github.com/moby/sys/signal"
"github.com/spf13/cobra"
@ -123,15 +122,15 @@ func addCompletions(cmd *cobra.Command, dockerCLI completion.APIClientProvider)
_ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
_ = cmd.RegisterFlagCompletionFunc("cgroupns", completeCgroupns())
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames())
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames())
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
_ = cmd.RegisterFlagCompletionFunc("ipc", completeIpc(dockerCLI))
_ = cmd.RegisterFlagCompletionFunc("link", completeLink(dockerCLI))
_ = cmd.RegisterFlagCompletionFunc("log-driver", completeLogDriver(dockerCLI))
_ = cmd.RegisterFlagCompletionFunc("log-opt", completeLogOpt)
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCLI))
_ = cmd.RegisterFlagCompletionFunc("pid", completePid(dockerCLI))
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms())
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
_ = cmd.RegisterFlagCompletionFunc("security-opt", completeSecurityOpt)
@ -187,11 +186,11 @@ func completeLink(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
// of the build-in log drivers.
func completeLogDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().Info(cmd.Context(), client.InfoOptions{})
info, err := dockerCLI.Client().Info(cmd.Context())
if err != nil {
return builtInLogDrivers(), cobra.ShellCompDirectiveNoFileComp
}
drivers := res.Info.Plugins.Log
drivers := info.Plugins.Log
return drivers, cobra.ShellCompDirectiveNoFileComp
}
}
@ -280,12 +279,12 @@ func completeUlimit(_ *cobra.Command, _ []string, _ string) ([]string, cobra.She
// completeVolumeDriver contacts the API to get the built-in and installed volume drivers.
func completeVolumeDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().Info(cmd.Context(), client.InfoOptions{})
info, err := dockerCLI.Client().Info(cmd.Context())
if err != nil {
// fallback: the built-in drivers
return []string{"local"}, cobra.ShellCompDirectiveNoFileComp
}
drivers := res.Info.Plugins.Volume
drivers := info.Plugins.Volume
return drivers, cobra.ShellCompDirectiveNoFileComp
}
}

View File

@ -6,8 +6,7 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/builders"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"github.com/moby/sys/signal"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
@ -27,7 +26,7 @@ func TestCompleteLinuxCapabilityNames(t *testing.T) {
func TestCompletePid(t *testing.T) {
tests := []struct {
containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error)
containerListFunc func(container.ListOptions) ([]container.Summary, error)
toComplete string
expectedCompletions []string
expectedDirective cobra.ShellCompDirective
@ -43,12 +42,10 @@ func TestCompletePid(t *testing.T) {
expectedDirective: cobra.ShellCompDirectiveNoSpace,
},
{
containerListFunc: func(client.ContainerListOptions) (client.ContainerListResult, error) {
return client.ContainerListResult{
Items: []container.Summary{
*builders.Container("c1"),
*builders.Container("c2"),
},
containerListFunc: func(container.ListOptions) ([]container.Summary, error) {
return []container.Summary{
*builders.Container("c1"),
*builders.Container("c2"),
}, nil
},
toComplete: "container:",

View File

@ -3,7 +3,6 @@ package container
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
@ -16,10 +15,11 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/streams"
"github.com/docker/docker/api/types/container"
"github.com/docker/go-units"
"github.com/moby/go-archive"
"github.com/moby/moby/client"
"github.com/morikuni/aec"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -121,7 +121,13 @@ func copyProgress(ctx context.Context, dst io.Writer, header string, total *int6
return restore, done
}
// newCopyCommand creates a new `docker cp` command
// NewCopyCommand creates a new `docker cp` command
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewCopyCommand(dockerCLI command.Cli) *cobra.Command {
return newCopyCommand(dockerCLI)
}
func newCopyCommand(dockerCLI command.Cli) *cobra.Command {
var opts copyOptions
@ -154,7 +160,6 @@ container source to stdout.`,
Annotations: map[string]string{
"aliases": "docker container cp, docker cp",
},
DisableFlagsInUseLine: true,
}
flags := cmd.Flags()
@ -230,13 +235,11 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
// if client requests to follow symbol link, then must decide target file to be copied
var rebaseName string
if copyConfig.followLink {
src, err := apiClient.ContainerStatPath(ctx, copyConfig.container, client.ContainerStatPathOptions{
Path: srcPath,
})
srcStat, err := apiClient.ContainerStatPath(ctx, copyConfig.container, srcPath)
// If the destination is a symbolic link, we should follow it.
if err == nil && src.Stat.Mode&os.ModeSymlink != 0 {
linkTarget := src.Stat.LinkTarget
if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
linkTarget := srcStat.LinkTarget
if !isAbs(linkTarget) {
// Join with the parent directory.
srcParent, _ := archive.SplitPathDirEntry(srcPath)
@ -251,14 +254,11 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
defer cancel()
cpRes, err := apiClient.CopyFromContainer(ctx, copyConfig.container, client.CopyFromContainerOptions{
SourcePath: srcPath,
})
content, stat, err := apiClient.CopyFromContainer(ctx, copyConfig.container, srcPath)
if err != nil {
return err
}
content := cpRes.Content
defer func() { _ = content.Close() }()
defer content.Close()
if dstPath == "-" {
_, err = io.Copy(dockerCLI.Out(), content)
@ -268,7 +268,7 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
srcInfo := archive.CopyInfo{
Path: srcPath,
Exists: true,
IsDir: cpRes.Stat.Mode.IsDir(),
IsDir: stat.Mode.IsDir(),
RebaseName: rebaseName,
}
@ -304,50 +304,50 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
// about both the source and destination. The API is a simple tar
// archive/extract API but we can use the stat info header about the
// destination to be more informed about exactly what the destination is.
func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpConfig) error {
func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpConfig) (err error) {
srcPath := copyConfig.sourcePath
dstPath := copyConfig.destPath
if srcPath != "-" {
// Get an absolute source path.
p, err := resolveLocalPath(srcPath)
srcPath, err = resolveLocalPath(srcPath)
if err != nil {
return err
}
srcPath = p
}
apiClient := dockerCLI.Client()
// Prepare destination copy info by stat-ing the container path.
dstInfo := archive.CopyInfo{Path: dstPath}
if dst, err := apiClient.ContainerStatPath(ctx, copyConfig.container, client.ContainerStatPathOptions{Path: dstPath}); err == nil {
// If the destination is a symbolic link, we should evaluate it.
if dst.Stat.Mode&os.ModeSymlink != 0 {
linkTarget := dst.Stat.LinkTarget
if !isAbs(linkTarget) {
// Join with the parent directory.
dstParent, _ := archive.SplitPathDirEntry(dstPath)
linkTarget = filepath.Join(dstParent, linkTarget)
}
dstStat, err := apiClient.ContainerStatPath(ctx, copyConfig.container, dstPath)
dstInfo.Path = linkTarget
dst, err = apiClient.ContainerStatPath(ctx, copyConfig.container, client.ContainerStatPathOptions{Path: linkTarget})
}
// Validate the destination path
if err == nil {
if err := command.ValidateOutputPathFileMode(dst.Stat.Mode); err != nil {
return fmt.Errorf(`destination "%s:%s" must be a directory or a regular file: %w`, copyConfig.container, dstPath, err)
}
dstInfo.Exists, dstInfo.IsDir = true, dst.Stat.Mode.IsDir()
// If the destination is a symbolic link, we should evaluate it.
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
linkTarget := dstStat.LinkTarget
if !isAbs(linkTarget) {
// Join with the parent directory.
dstParent, _ := archive.SplitPathDirEntry(dstPath)
linkTarget = filepath.Join(dstParent, linkTarget)
}
// Ignore any error and assume that the parent directory of the destination
// path exists, in which case the copy may still succeed. If there is any
// type of conflict (e.g., non-directory overwriting an existing directory
// or vice versa) the extraction will fail. If the destination simply did
// not exist, but the parent directory does, the extraction will still
// succeed.
_ = err // Intentionally ignore stat errors (see above)
dstInfo.Path = linkTarget
dstStat, err = apiClient.ContainerStatPath(ctx, copyConfig.container, linkTarget)
// FIXME(thaJeztah): unhandled error (should this return?)
}
// Validate the destination path
if err := command.ValidateOutputPathFileMode(dstStat.Mode); err != nil {
return errors.Wrapf(err, `destination "%s:%s" must be a directory or a regular file`, copyConfig.container, dstPath)
}
// Ignore any error and assume that the parent directory of the destination
// path exists, in which case the copy may still succeed. If there is any
// type of conflict (e.g., non-directory overwriting an existing directory
// or vice versa) the extraction will fail. If the destination simply did
// not exist, but the parent directory does, the extraction will still
// succeed.
if err == nil {
dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
}
var (
@ -360,7 +360,7 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
content = os.Stdin
resolvedDstPath = dstInfo.Path
if !dstInfo.IsDir {
return fmt.Errorf(`destination "%s:%s" must be a directory`, copyConfig.container, dstPath)
return errors.Errorf("destination \"%s:%s\" must be a directory", copyConfig.container, dstPath)
}
} else {
// Prepare source copy info.
@ -403,27 +403,23 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
}
}
options := client.CopyToContainerOptions{
DestinationPath: resolvedDstPath,
Content: content,
CopyUIDGID: copyConfig.copyUIDGID,
options := container.CopyToContainerOptions{
CopyUIDGID: copyConfig.copyUIDGID,
}
if copyConfig.quiet {
_, err := apiClient.CopyToContainer(ctx, copyConfig.container, options)
return err
return apiClient.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
}
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
restore, done := copyProgress(ctx, dockerCLI.Err(), copyToContainerHeader, &copiedSize)
// TODO(thaJeztah): error-handling looks odd here; should it be handled differently?
_, err := apiClient.CopyToContainer(ctx, copyConfig.container, options)
res := apiClient.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
cancel()
<-done
restore()
_, _ = fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path)
fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path)
return err
return res
}
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be

View File

@ -9,9 +9,9 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/container"
"github.com/moby/go-archive"
"github.com/moby/go-archive/compression"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/fs"
@ -52,11 +52,9 @@ func TestRunCopyFromContainerToStdout(t *testing.T) {
tarContent := "the tar content"
cli := test.NewFakeCli(&fakeClient{
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
assert.Check(t, is.Equal("container", ctr))
return client.CopyFromContainerResult{
Content: io.NopCloser(strings.NewReader(tarContent)),
}, nil
return io.NopCloser(strings.NewReader(tarContent)), container.PathStat{}, nil
},
})
err := runCopy(context.TODO(), cli, copyOptions{
@ -75,12 +73,10 @@ func TestRunCopyFromContainerToFilesystem(t *testing.T) {
destDir := fs.NewDir(t, "cp-test")
cli := test.NewFakeCli(&fakeClient{
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
assert.Check(t, is.Equal("container", ctr))
readCloser, err := archive.Tar(srcDir.Path(), compression.None)
return client.CopyFromContainerResult{
Content: readCloser,
}, err
return readCloser, container.PathStat{}, err
},
})
err := runCopy(context.TODO(), cli, copyOptions{
@ -103,12 +99,10 @@ func TestRunCopyFromContainerToFilesystemMissingDestinationDirectory(t *testing.
defer destDir.Remove()
cli := test.NewFakeCli(&fakeClient{
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
assert.Check(t, is.Equal("container", ctr))
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
return client.CopyFromContainerResult{
Content: readCloser,
}, err
return readCloser, container.PathStat{}, err
},
})
err := runCopy(context.TODO(), cli, copyOptions{

View File

@ -4,9 +4,9 @@ import (
"archive/tar"
"bytes"
"context"
"errors"
"fmt"
"io"
"net/netip"
"os"
"path"
"strings"
@ -17,14 +17,20 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/jsonstream"
"github.com/docker/cli/opts"
"github.com/moby/moby/api/types/mount"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
imagetypes "github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
@ -39,12 +45,19 @@ const (
type createOptions struct {
name string
platform string
untrusted bool
pull string // always, missing, never
quiet bool
useAPISocket bool
}
// newCreateCommand creates a new cobra.Command for `docker create`
// NewCreateCommand creates a new cobra.Command for `docker create`
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewCreateCommand(dockerCLI command.Cli) *cobra.Command {
return newCreateCommand(dockerCLI)
}
func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
var options createOptions
var copts *containerOptions
@ -63,8 +76,7 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
Annotations: map[string]string{
"aliases": "docker container create, docker create",
},
ValidArgsFunction: completion.ImageNames(dockerCLI, -1),
DisableFlagsInUseLine: true,
ValidArgsFunction: completion.ImageNames(dockerCLI, -1),
}
flags := cmd.Flags()
@ -74,24 +86,28 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before creating ("`+PullImageAlways+`", "|`+PullImageMissing+`", "`+PullImageNever+`")`)
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output")
flags.BoolVarP(&options.useAPISocket, "use-api-socket", "", false, "Bind mount Docker API socket and required auth")
_ = flags.SetAnnotation("use-api-socket", "experimentalCLI", nil) // Mark flag as experimental for now.
flags.SetAnnotation("use-api-socket", "experimentalCLI", nil) // Marks flag as experimental for now.
// Add an explicit help that doesn't have a `-h` to prevent the conflict
// with hostname
flags.Bool("help", false, "Print usage")
// TODO(thaJeztah): consider adding platform as "image create option" on containerOptions
flags.StringVar(&options.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms())
addPlatformFlag(flags, &options.platform)
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
// TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30
flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)")
_ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed")
flags.BoolVar(&options.untrusted, "disable-content-trust", !dockerCLI.ContentTrustEnabled(), "Skip image verification")
copts = addFlags(flags)
addCompletions(cmd, dockerCLI)
flags.VisitAll(func(flag *pflag.Flag) {
// Set a default completion function if none was set. We don't look
// up if it does already have one set, because Cobra does this for
// us, and returns an error (which we ignore for this reason).
_ = cmd.RegisterFlagCompletionFunc(flag.Name, cobra.NoFileCompletions)
})
return cmd
}
@ -103,7 +119,7 @@ func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet,
}
}
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetSlice()))
newEnv := make([]string, 0, len(proxyConfig))
newEnv := []string{}
for k, v := range proxyConfig {
if v == nil {
newEnv = append(newEnv, k)
@ -134,27 +150,20 @@ func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *
return err
}
var ociPlatforms []ocispec.Platform
if options.platform != "" {
// Already validated.
ociPlatforms = append(ociPlatforms, platforms.MustParse(options.platform))
}
resp, err := dockerCli.Client().ImagePull(ctx, img, client.ImagePullOptions{
responseBody, err := dockerCli.Client().ImageCreate(ctx, img, imagetypes.CreateOptions{
RegistryAuth: encodedAuth,
Platforms: ociPlatforms,
Platform: options.platform,
})
if err != nil {
return err
}
defer func() {
_ = resp.Close()
}()
defer responseBody.Close()
out := dockerCli.Err()
if options.quiet {
out = streams.NewOut(io.Discard)
}
return jsonstream.Display(ctx, resp, out)
return jsonstream.Display(ctx, responseBody, out)
}
type cidFile struct {
@ -167,13 +176,13 @@ func (cid *cidFile) Close() error {
if cid.file == nil {
return nil
}
_ = cid.file.Close()
cid.file.Close()
if cid.written {
return nil
}
if err := os.Remove(cid.path); err != nil {
return fmt.Errorf("failed to remove the CID file '%s': %w", cid.path, err)
return errors.Wrapf(err, "failed to remove the CID file '%s'", cid.path)
}
return nil
@ -184,7 +193,7 @@ func (cid *cidFile) Write(id string) error {
return nil
}
if _, err := cid.file.Write([]byte(id)); err != nil {
return fmt.Errorf("failed to write the container ID to the file: %w", err)
return errors.Wrap(err, "failed to write the container ID to the file")
}
cid.written = true
return nil
@ -195,12 +204,12 @@ func newCIDFile(cidPath string) (*cidFile, error) {
return &cidFile{}, nil
}
if _, err := os.Stat(cidPath); err == nil {
return nil, errors.New("container ID file found, make sure the other container isn't running or delete " + cidPath)
return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", cidPath)
}
f, err := os.Create(cidPath)
if err != nil {
return nil, fmt.Errorf("failed to create the container ID file: %w", err)
return nil, errors.Wrap(err, "failed to create the container ID file")
}
return &cidFile{path: cidPath, file: f}, nil
@ -212,23 +221,16 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
hostConfig := containerCfg.HostConfig
networkingConfig := containerCfg.NetworkingConfig
var namedRef reference.Named
// TODO(thaJeztah): add a platform option-type / flag-type.
if options.platform != "" {
_, err = platforms.Parse(options.platform)
if err != nil {
return "", err
}
}
var (
trustedRef reference.Canonical
namedRef reference.Named
)
containerIDFile, err := newCIDFile(hostConfig.ContainerIDFile)
if err != nil {
return "", err
}
defer func() {
_ = containerIDFile.Close()
}()
defer containerIDFile.Close()
ref, err := reference.ParseAnyReference(config.Image)
if err != nil {
@ -236,6 +238,15 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
}
if named, ok := ref.(reference.Named); ok {
namedRef = reference.TagNameOnly(named)
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && !options.untrusted {
var err error
trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef)
if err != nil {
return "", err
}
config.Image = reference.FamiliarString(trustedRef)
}
}
const dockerConfigPathInContainer = "/run/secrets/docker/config.json"
@ -306,10 +317,14 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
}
var platform *ocispec.Platform
if options.platform != "" {
// Engine API version 1.41 first introduced the option to specify platform on
// create. It will produce an error if you try to set a platform on older API
// versions, so check the API version here to maintain backwards
// compatibility for CLI users.
if options.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") {
p, err := platforms.Parse(options.platform)
if err != nil {
return "", invalidParameter(fmt.Errorf("error parsing specified platform: %w", err))
return "", errors.Wrap(invalidParameter(err), "error parsing specified platform")
}
platform = &p
}
@ -318,6 +333,9 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
if err := pullImage(ctx, dockerCli, config.Image, options); err != nil {
return err
}
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
return trust.TagTrusted(ctx, dockerCli.Client(), dockerCli.Err(), trustedRef, taggedRef)
}
return nil
}
@ -329,14 +347,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize()
response, err := dockerCli.Client().ContainerCreate(ctx, client.ContainerCreateOptions{
Name: options.name,
// Image: config.Image, // TODO(thaJeztah): pass image-ref separate
Platform: platform,
Config: config,
HostConfig: hostConfig,
NetworkingConfig: networkingConfig,
})
response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
if err != nil {
// Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior.
if errdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing {
@ -350,14 +361,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
}
var retryErr error
response, retryErr = dockerCli.Client().ContainerCreate(ctx, client.ContainerCreateOptions{
Name: options.name,
// Image: config.Image, // TODO(thaJeztah): pass image-ref separate
Platform: platform,
Config: config,
HostConfig: hostConfig,
NetworkingConfig: networkingConfig,
})
response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
if retryErr != nil {
return "", retryErr
}
@ -366,6 +370,10 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
}
}
if warn := localhostDNSWarning(*hostConfig); warn != "" {
response.Warnings = append(response.Warnings, warn)
}
containerID = response.ID
for _, w := range response.Warnings {
_, _ = fmt.Fprintln(dockerCli.Err(), "WARNING:", w)
@ -386,6 +394,19 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
return containerID, err
}
// check the DNS settings passed via --dns against localhost regexp to warn if
// they are trying to set a DNS to a localhost address.
//
// TODO(thaJeztah): move this to the daemon, which can make a better call if it will work or not (depending on networking mode).
func localhostDNSWarning(hostConfig container.HostConfig) string {
for _, dnsIP := range hostConfig.DNS {
if addr, err := netip.ParseAddr(dnsIP); err == nil && addr.IsLoopback() {
return fmt.Sprintf("Localhost DNS (%s) may fail in containers.", addr)
}
}
return ""
}
func validatePullOpt(val string) error {
switch val {
case PullImageAlways, PullImageMissing, PullImageNever, "":
@ -407,7 +428,7 @@ func validatePullOpt(val string) error {
//
// The path should be an absolute path in the container, commonly
// /root/.docker/config.json.
func copyDockerConfigIntoContainer(ctx context.Context, apiClient client.APIClient, containerID string, configPath string, config *configfile.ConfigFile) error {
func copyDockerConfigIntoContainer(ctx context.Context, dockerAPI client.APIClient, containerID string, configPath string, config *configfile.ConfigFile) error {
var configBuf bytes.Buffer
if err := config.SaveToWriter(&configBuf); err != nil {
return fmt.Errorf("saving creds: %w", err)
@ -416,7 +437,7 @@ func copyDockerConfigIntoContainer(ctx context.Context, apiClient client.APIClie
// We don't need to get super fancy with the tar creation.
var tarBuf bytes.Buffer
tarWriter := tar.NewWriter(&tarBuf)
_ = tarWriter.WriteHeader(&tar.Header{
tarWriter.WriteHeader(&tar.Header{
Name: configPath,
Size: int64(configBuf.Len()),
Mode: 0o600,
@ -430,11 +451,8 @@ func copyDockerConfigIntoContainer(ctx context.Context, apiClient client.APIClie
return fmt.Errorf("closing tar for config copy failed: %w", err)
}
_, err := apiClient.CopyToContainer(ctx, containerID, client.CopyToContainerOptions{
DestinationPath: "/",
Content: &tarBuf,
})
if err != nil {
if err := dockerAPI.CopyToContainer(ctx, containerID, "/",
&tarBuf, container.CopyToContainerOptions{}); err != nil {
return fmt.Errorf("copying config.json into container failed: %w", err)
}

View File

@ -13,10 +13,13 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/system"
"github.com/google/go-cmp/cmp"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/system"
"github.com/moby/moby/client"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/pflag"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
@ -114,30 +117,35 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
pullCounter := 0
apiClient := &fakeClient{
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
createContainerFunc: func(
config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
platform *ocispec.Platform,
containerName string,
) (container.CreateResponse, error) {
defer func() { tc.ResponseCounter++ }()
switch tc.ResponseCounter {
case 0:
return client.ContainerCreateResult{}, fakeNotFound{}
return container.CreateResponse{}, fakeNotFound{}
default:
return client.ContainerCreateResult{ID: containerID}, nil
return container.CreateResponse{ID: containerID}, nil
}
},
imagePullFunc: func(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
imageCreateFunc: func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
defer func() { pullCounter++ }()
return fakeStreamResult{ReadCloser: io.NopCloser(strings.NewReader(""))}, nil
return io.NopCloser(strings.NewReader("")), nil
},
infoFunc: func() (client.SystemInfoResult, error) {
return client.SystemInfoResult{
Info: system.Info{IndexServerAddress: "https://indexserver.example.com"},
}, nil
infoFunc: func() (system.Info, error) {
return system.Info{IndexServerAddress: "https://indexserver.example.com"}, nil
},
}
fakeCLI := test.NewFakeCli(apiClient)
id, err := createContainer(context.Background(), fakeCLI, config, &createOptions{
name: "name",
platform: runtime.GOOS,
pull: tc.PullPolicy,
name: "name",
platform: runtime.GOOS,
untrusted: true,
pull: tc.PullPolicy,
})
if tc.ExpectedErrMsg != "" {
@ -213,6 +221,55 @@ func TestCreateContainerValidateFlags(t *testing.T) {
}
}
func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
testCases := []struct {
name string
args []string
expectedError string
notaryFunc test.NotaryClientFuncType
}{
{
name: "offline-notary-server",
notaryFunc: notary.GetOfflineNotaryRepository,
expectedError: "client is offline",
args: []string{"image:tag"},
},
{
name: "uninitialized-notary-server",
notaryFunc: notary.GetUninitializedNotaryRepository,
expectedError: "remote trust data does not exist",
args: []string{"image:tag"},
},
{
name: "empty-notary-server",
notaryFunc: notary.GetEmptyTargetsNotaryRepository,
expectedError: "No valid trust data for tag",
args: []string{"image:tag"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fakeCLI := test.NewFakeCli(&fakeClient{
createContainerFunc: func(config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
platform *ocispec.Platform,
containerName string,
) (container.CreateResponse, error) {
return container.CreateResponse{}, errors.New("shouldn't try to pull image")
},
}, test.EnableContentTrust)
fakeCLI.SetNotaryClient(tc.notaryFunc)
cmd := newCreateCommand(fakeCLI)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.ErrorContains(t, err, tc.expectedError)
})
}
}
func TestNewCreateCommandWithWarnings(t *testing.T) {
testCases := []struct {
name string
@ -234,12 +291,27 @@ func TestNewCreateCommandWithWarnings(t *testing.T) {
args: []string{"image:tag"},
warnings: []string{"warning from daemon", "another warning from daemon"},
},
{
name: "container-create-localhost-dns",
args: []string{"--dns=127.0.0.11", "image:tag"},
warning: true,
},
{
name: "container-create-localhost-dns-ipv6",
args: []string{"--dns=::1", "image:tag"},
warning: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fakeCLI := test.NewFakeCli(&fakeClient{
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
return client.ContainerCreateResult{Warnings: tc.warnings}, nil
createContainerFunc: func(config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
platform *ocispec.Platform,
containerName string,
) (container.CreateResponse, error) {
return container.CreateResponse{Warnings: tc.warnings}, nil
},
})
cmd := newCreateCommand(fakeCLI)
@ -272,10 +344,15 @@ func TestCreateContainerWithProxyConfig(t *testing.T) {
sort.Strings(expected)
fakeCLI := test.NewFakeCli(&fakeClient{
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
sort.Strings(options.Config.Env)
assert.DeepEqual(t, options.Config.Env, expected)
return client.ContainerCreateResult{}, nil
createContainerFunc: func(config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
platform *ocispec.Platform,
containerName string,
) (container.CreateResponse, error) {
sort.Strings(config.Env)
assert.DeepEqual(t, config.Env, expected)
return container.CreateResponse{}, nil
},
})
fakeCLI.SetConfigFile(&configfile.ConfigFile{

View File

@ -7,11 +7,16 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
// newDiffCommand creates a new cobra.Command for `docker diff`
// NewDiffCommand creates a new cobra.Command for `docker diff`
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewDiffCommand(dockerCLI command.Cli) *cobra.Command {
return newDiffCommand(dockerCLI)
}
func newDiffCommand(dockerCLI command.Cli) *cobra.Command {
return &cobra.Command{
Use: "diff CONTAINER",
@ -23,13 +28,12 @@ func newDiffCommand(dockerCLI command.Cli) *cobra.Command {
Annotations: map[string]string{
"aliases": "docker container diff, docker diff",
},
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
DisableFlagsInUseLine: true,
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
}
}
func runDiff(ctx context.Context, dockerCLI command.Cli, containerID string) error {
res, err := dockerCLI.Client().ContainerDiff(ctx, containerID, client.ContainerDiffOptions{})
changes, err := dockerCLI.Client().ContainerDiff(ctx, containerID)
if err != nil {
return err
}
@ -37,5 +41,5 @@ func runDiff(ctx context.Context, dockerCLI command.Cli, containerID string) err
Output: dockerCLI.Out(),
Format: newDiffFormat("{{.Type}} {{.Path}}"),
}
return diffFormatWrite(diffCtx, res)
return diffFormatWrite(diffCtx, changes)
}

View File

@ -8,29 +8,29 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestRunDiff(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerDiffFunc: func(ctx context.Context, containerID string) (client.ContainerDiffResult, error) {
return client.ContainerDiffResult{
Changes: []container.FilesystemChange{
{
Kind: container.ChangeModify,
Path: "/path/to/file0",
},
{
Kind: container.ChangeAdd,
Path: "/path/to/file1",
},
{
Kind: container.ChangeDelete,
Path: "/path/to/file2",
},
containerDiffFunc: func(
ctx context.Context,
containerID string,
) ([]container.FilesystemChange, error) {
return []container.FilesystemChange{
{
Kind: container.ChangeModify,
Path: "/path/to/file0",
},
{
Kind: container.ChangeAdd,
Path: "/path/to/file1",
},
{
Kind: container.ChangeDelete,
Path: "/path/to/file2",
},
}, nil
},
@ -60,8 +60,11 @@ func TestRunDiffClientError(t *testing.T) {
clientError := errors.New("client error")
cli := test.NewFakeCli(&fakeClient{
containerDiffFunc: func(ctx context.Context, containerID string) (client.ContainerDiffResult, error) {
return client.ContainerDiffResult{}, clientError
containerDiffFunc: func(
ctx context.Context,
containerID string,
) ([]container.FilesystemChange, error) {
return nil, clientError
},
})

View File

@ -2,7 +2,6 @@ package container
import (
"context"
"errors"
"fmt"
"io"
@ -11,8 +10,9 @@ import (
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/opts"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@ -39,7 +39,13 @@ func NewExecOptions() ExecOptions {
}
}
// newExecCommand creates a new cobra.Command for "docker exec".
// NewExecCommand creates a new cobra.Command for `docker exec`
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewExecCommand(dockerCLI command.Cli) *cobra.Command {
return newExecCommand(dockerCLI)
}
func newExecCommand(dockerCLI command.Cli) *cobra.Command {
options := NewExecOptions()
@ -59,7 +65,6 @@ func newExecCommand(dockerCLI command.Cli) *cobra.Command {
"category-top": "2",
"aliases": "docker container exec, docker exec",
},
DisableFlagsInUseLine: true,
}
flags := cmd.Flags()
@ -72,14 +77,14 @@ func newExecCommand(dockerCLI command.Cli) *cobra.Command {
flags.StringVarP(&options.User, "user", "u", "", `Username or UID (format: "<name|uid>[:<group|gid>]")`)
flags.BoolVar(&options.Privileged, "privileged", false, "Give extended privileges to the command")
flags.VarP(&options.Env, "env", "e", "Set environment variables")
_ = flags.SetAnnotation("env", "version", []string{"1.25"})
flags.SetAnnotation("env", "version", []string{"1.25"})
flags.Var(&options.EnvFile, "env-file", "Read in a file of environment variables")
_ = flags.SetAnnotation("env-file", "version", []string{"1.25"})
flags.SetAnnotation("env-file", "version", []string{"1.25"})
flags.StringVarP(&options.Workdir, "workdir", "w", "", "Working directory inside the container")
_ = flags.SetAnnotation("workdir", "version", []string{"1.35"})
flags.SetAnnotation("workdir", "version", []string{"1.35"})
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames())
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames())
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
return cmd
}
@ -97,18 +102,18 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin
// otherwise if we error out we will leak execIDs on the server (and
// there's no easy way to clean those up). But also in order to make "not
// exist" errors take precedence we do a dummy inspect first.
if _, err := apiClient.ContainerInspect(ctx, containerIDorName, client.ContainerInspectOptions{}); err != nil {
if _, err := apiClient.ContainerInspect(ctx, containerIDorName); err != nil {
return err
}
if !options.Detach {
if err := dockerCLI.In().CheckTty(execOptions.AttachStdin, execOptions.TTY); err != nil {
if err := dockerCLI.In().CheckTty(execOptions.AttachStdin, execOptions.Tty); err != nil {
return err
}
}
fillConsoleSize(execOptions, dockerCLI)
response, err := apiClient.ExecCreate(ctx, containerIDorName, *execOptions)
response, err := apiClient.ContainerExecCreate(ctx, containerIDorName, *execOptions)
if err != nil {
return err
}
@ -119,24 +124,23 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin
}
if options.Detach {
_, err := apiClient.ExecStart(ctx, execID, client.ExecStartOptions{
return apiClient.ContainerExecStart(ctx, execID, container.ExecStartOptions{
Detach: options.Detach,
TTY: execOptions.TTY,
ConsoleSize: client.ConsoleSize{Height: execOptions.ConsoleSize.Height, Width: execOptions.ConsoleSize.Width},
Tty: execOptions.Tty,
ConsoleSize: execOptions.ConsoleSize,
})
return err
}
return interactiveExec(ctx, dockerCLI, execOptions, execID)
}
func fillConsoleSize(execOptions *client.ExecCreateOptions, dockerCli command.Cli) {
if execOptions.TTY {
func fillConsoleSize(execOptions *container.ExecOptions, dockerCli command.Cli) {
if execOptions.Tty {
height, width := dockerCli.Out().GetTtySize()
execOptions.ConsoleSize = client.ConsoleSize{Height: height, Width: width}
execOptions.ConsoleSize = &[2]uint{height, width}
}
}
func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *client.ExecCreateOptions, execID string) error {
func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *container.ExecOptions, execID string) error {
// Interactive exec requested.
var (
out, stderr io.Writer
@ -150,7 +154,7 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *cl
out = dockerCli.Out()
}
if execOptions.AttachStderr {
if execOptions.TTY {
if execOptions.Tty {
stderr = dockerCli.Out()
} else {
stderr = dockerCli.Err()
@ -159,9 +163,9 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *cl
fillConsoleSize(execOptions, dockerCli)
apiClient := dockerCli.Client()
resp, err := apiClient.ExecAttach(ctx, execID, client.ExecAttachOptions{
TTY: execOptions.TTY,
ConsoleSize: client.ConsoleSize{Height: execOptions.ConsoleSize.Height, Width: execOptions.ConsoleSize.Width},
resp, err := apiClient.ContainerExecAttach(ctx, execID, container.ExecAttachOptions{
Tty: execOptions.Tty,
ConsoleSize: execOptions.ConsoleSize,
})
if err != nil {
return err
@ -178,8 +182,8 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *cl
inputStream: in,
outputStream: out,
errorStream: stderr,
resp: resp.HijackedResponse,
tty: execOptions.TTY,
resp: resp,
tty: execOptions.Tty,
detachKeys: execOptions.DetachKeys,
}
@ -187,7 +191,7 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *cl
}()
}()
if execOptions.TTY && dockerCli.In().IsTerminal() {
if execOptions.Tty && dockerCli.In().IsTerminal() {
if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil {
_, _ = fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err)
}
@ -201,8 +205,8 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *cl
return getExecExitStatus(ctx, apiClient, execID)
}
func getExecExitStatus(ctx context.Context, apiClient client.ExecAPIClient, execID string) error {
resp, err := apiClient.ExecInspect(ctx, execID, client.ExecInspectOptions{})
func getExecExitStatus(ctx context.Context, apiClient client.ContainerAPIClient, execID string) error {
resp, err := apiClient.ContainerExecInspect(ctx, execID)
if err != nil {
// If we can't connect, then the daemon probably died.
if !client.IsErrConnectionFailed(err) {
@ -219,11 +223,11 @@ func getExecExitStatus(ctx context.Context, apiClient client.ExecAPIClient, exec
// parseExec parses the specified args for the specified command and generates
// an ExecConfig from it.
func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*client.ExecCreateOptions, error) {
execOptions := &client.ExecCreateOptions{
func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*container.ExecOptions, error) {
execOptions := &container.ExecOptions{
User: execOpts.User,
Privileged: execOpts.Privileged,
TTY: execOpts.TTY,
Tty: execOpts.TTY,
Cmd: execOpts.Command,
WorkingDir: execOpts.Workdir,
}

View File

@ -12,7 +12,7 @@ import (
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/opts"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/fs"
@ -38,10 +38,10 @@ TWO=2
testcases := []struct {
options ExecOptions
configFile configfile.ConfigFile
expected client.ExecCreateOptions
expected container.ExecOptions
}{
{
expected: client.ExecCreateOptions{
expected: container.ExecOptions{
Cmd: []string{"command"},
AttachStdout: true,
AttachStderr: true,
@ -49,7 +49,7 @@ TWO=2
options: withDefaultOpts(ExecOptions{}),
},
{
expected: client.ExecCreateOptions{
expected: container.ExecOptions{
Cmd: []string{"command1", "command2"},
AttachStdout: true,
AttachStderr: true,
@ -64,18 +64,18 @@ TWO=2
TTY: true,
User: "uid",
}),
expected: client.ExecCreateOptions{
expected: container.ExecOptions{
User: "uid",
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
TTY: true,
Tty: true,
Cmd: []string{"command"},
},
},
{
options: withDefaultOpts(ExecOptions{Detach: true}),
expected: client.ExecCreateOptions{
expected: container.ExecOptions{
Cmd: []string{"command"},
},
},
@ -85,15 +85,15 @@ TWO=2
Interactive: true,
Detach: true,
}),
expected: client.ExecCreateOptions{
TTY: true,
expected: container.ExecOptions{
Tty: true,
Cmd: []string{"command"},
},
},
{
options: withDefaultOpts(ExecOptions{Detach: true}),
configFile: configfile.ConfigFile{DetachKeys: "de"},
expected: client.ExecCreateOptions{
expected: container.ExecOptions{
Cmd: []string{"command"},
DetachKeys: "de",
},
@ -104,13 +104,13 @@ TWO=2
DetachKeys: "ab",
}),
configFile: configfile.ConfigFile{DetachKeys: "de"},
expected: client.ExecCreateOptions{
expected: container.ExecOptions{
Cmd: []string{"command"},
DetachKeys: "ab",
},
},
{
expected: client.ExecCreateOptions{
expected: container.ExecOptions{
Cmd: []string{"command"},
AttachStdout: true,
AttachStderr: true,
@ -118,12 +118,12 @@ TWO=2
},
options: func() ExecOptions {
o := withDefaultOpts(ExecOptions{})
_ = o.EnvFile.Set(tmpFile.Path())
o.EnvFile.Set(tmpFile.Path())
return o
}(),
},
{
expected: client.ExecCreateOptions{
expected: container.ExecOptions{
Cmd: []string{"command"},
AttachStdout: true,
AttachStderr: true,
@ -131,8 +131,8 @@ TWO=2
},
options: func() ExecOptions {
o := withDefaultOpts(ExecOptions{})
_ = o.EnvFile.Set(tmpFile.Path())
_ = o.Env.Set("ONE=override")
o.EnvFile.Set(tmpFile.Path())
o.Env.Set("ONE=override")
return o
}(),
},
@ -149,7 +149,7 @@ TWO=2
func TestParseExecNoSuchFile(t *testing.T) {
execOpts := withDefaultOpts(ExecOptions{})
assert.Check(t, execOpts.EnvFile.Set("no-such-env-file"))
execOpts.EnvFile.Set("no-such-env-file")
execConfig, err := parseExec(execOpts, &configfile.ConfigFile{})
assert.ErrorContains(t, err, "no-such-env-file")
assert.Check(t, os.IsNotExist(err))
@ -176,8 +176,8 @@ func TestRunExec(t *testing.T) {
doc: "inspect error",
options: NewExecOptions(),
client: &fakeClient{
inspectFunc: func(string) (client.ContainerInspectResult, error) {
return client.ContainerInspectResult{}, errors.New("failed inspect")
inspectFunc: func(string) (container.InspectResponse, error) {
return container.InspectResponse{}, errors.New("failed inspect")
},
},
expectedError: "failed inspect",
@ -194,7 +194,7 @@ func TestRunExec(t *testing.T) {
t.Run(testcase.doc, func(t *testing.T) {
fakeCLI := test.NewFakeCli(testcase.client)
err := RunExec(context.TODO(), fakeCLI, "the-container", testcase.options)
err := RunExec(context.TODO(), fakeCLI, "thecontainer", testcase.options)
if testcase.expectedError != "" {
assert.ErrorContains(t, err, testcase.expectedError)
} else if !assert.Check(t, err) {
@ -206,8 +206,8 @@ func TestRunExec(t *testing.T) {
}
}
func execCreateWithID(_ string, _ client.ExecCreateOptions) (client.ExecCreateResult, error) {
return client.ExecCreateResult{ID: "exec-id"}, nil
func execCreateWithID(_ string, _ container.ExecOptions) (container.ExecCreateResponse, error) {
return container.ExecCreateResponse{ID: "execid"}, nil
}
func TestGetExecExitStatus(t *testing.T) {
@ -235,9 +235,9 @@ func TestGetExecExitStatus(t *testing.T) {
for _, testcase := range testcases {
apiClient := &fakeClient{
execInspectFunc: func(id string) (client.ExecInspectResult, error) {
execInspectFunc: func(id string) (container.ExecInspect, error) {
assert.Check(t, is.Equal(execID, id))
return client.ExecInspectResult{ExitCode: testcase.exitCode}, testcase.inspectError
return container.ExecInspect{ExitCode: testcase.exitCode}, testcase.inspectError
},
}
err := getExecExitStatus(context.Background(), apiClient, execID)
@ -250,14 +250,14 @@ func TestNewExecCommandErrors(t *testing.T) {
name string
args []string
expectedError string
containerInspectFunc func(img string) (client.ContainerInspectResult, error)
containerInspectFunc func(img string) (container.InspectResponse, error)
}{
{
name: "client-error",
args: []string{"5cb5bb5e4a3b", "-t", "-i", "bash"},
expectedError: "something went wrong",
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
return client.ContainerInspectResult{}, errors.New("something went wrong")
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
return container.InspectResponse{}, errors.New("something went wrong")
},
},
}

View File

@ -2,15 +2,13 @@ package container
import (
"context"
"errors"
"fmt"
"io"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/moby/moby/client"
"github.com/moby/sys/atomicwriter"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -19,7 +17,13 @@ type exportOptions struct {
output string
}
// newExportCommand creates a new "docker container export" command.
// NewExportCommand creates a new `docker export` command
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewExportCommand(dockerCLI command.Cli) *cobra.Command {
return newExportCommand(dockerCLI)
}
func newExportCommand(dockerCLI command.Cli) *cobra.Command {
var opts exportOptions
@ -34,8 +38,7 @@ func newExportCommand(dockerCLI command.Cli) *cobra.Command {
Annotations: map[string]string{
"aliases": "docker container export, docker export",
},
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
DisableFlagsInUseLine: true,
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
}
flags := cmd.Flags()
@ -55,13 +58,13 @@ func runExport(ctx context.Context, dockerCLI command.Cli, opts exportOptions) e
} else {
writer, err := atomicwriter.New(opts.output, 0o600)
if err != nil {
return fmt.Errorf("failed to export container: %w", err)
return errors.Wrap(err, "failed to export container")
}
defer writer.Close()
output = writer
}
responseBody, err := dockerCLI.Client().ContainerExport(ctx, opts.container, client.ContainerExportOptions{})
responseBody, err := dockerCLI.Client().ContainerExport(ctx, opts.container)
if err != nil {
return err
}

View File

@ -2,10 +2,10 @@ package container
import (
"io"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
"gotest.tools/v3/fs"
)
@ -15,9 +15,8 @@ func TestContainerExportOutputToFile(t *testing.T) {
defer dir.Remove()
cli := test.NewFakeCli(&fakeClient{
containerExportFunc: func(container string) (client.ContainerExportResult, error) {
// FIXME(thaJeztah): how to mock this?
return mockContainerExportResult("bar"), nil
containerExportFunc: func(container string) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("bar")), nil
},
})
cmd := newExportCommand(cli)
@ -34,9 +33,8 @@ func TestContainerExportOutputToFile(t *testing.T) {
func TestContainerExportOutputToIrregularFile(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerExportFunc: func(container string) (client.ContainerExportResult, error) {
// FIXME(thaJeztah): how to mock this?
return mockContainerExportResult("foo"), nil
containerExportFunc: func(container string) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("foo")), nil
},
})
cmd := newExportCommand(cli)

View File

@ -2,8 +2,7 @@ package container
import (
"github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
)
const (
@ -13,6 +12,13 @@ const (
pathHeader = "PATH"
)
// NewDiffFormat returns a format for use with a diff Context
//
// Deprecated: this function was only used internally and will be removed in the next release.
func NewDiffFormat(source string) formatter.Format {
return newDiffFormat(source)
}
// newDiffFormat returns a format for use with a diff [formatter.Context].
func newDiffFormat(source string) formatter.Format {
if source == formatter.TableFormatKey {
@ -21,10 +27,17 @@ func newDiffFormat(source string) formatter.Format {
return formatter.Format(source)
}
// DiffFormatWrite writes formatted diff using the Context
//
// Deprecated: this function was only used internally and will be removed in the next release.
func DiffFormatWrite(fmtCtx formatter.Context, changes []container.FilesystemChange) error {
return diffFormatWrite(fmtCtx, changes)
}
// diffFormatWrite writes formatted diff using the [formatter.Context].
func diffFormatWrite(fmtCtx formatter.Context, changes client.ContainerDiffResult) error {
func diffFormatWrite(fmtCtx formatter.Context, changes []container.FilesystemChange) error {
return fmtCtx.Write(newDiffContext(), func(format func(subContext formatter.SubContext) error) error {
for _, change := range changes.Changes {
for _, change := range changes {
if err := format(&diffContext{c: change}); err != nil {
return err
}

View File

@ -5,8 +5,7 @@ import (
"testing"
"github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"gotest.tools/v3/assert"
)
@ -41,12 +40,10 @@ D: /usr/app/old_app.js
},
}
diffs := client.ContainerDiffResult{
Changes: []container.FilesystemChange{
{Kind: container.ChangeModify, Path: "/var/log/app.log"},
{Kind: container.ChangeAdd, Path: "/usr/app/app.js"},
{Kind: container.ChangeDelete, Path: "/usr/app/old_app.js"},
},
diffs := []container.FilesystemChange{
{Kind: container.ChangeModify, Path: "/var/log/app.log"},
{Kind: container.ChangeAdd, Path: "/usr/app/app.js"},
{Kind: container.ChangeDelete, Path: "/usr/app/old_app.js"},
}
for _, tc := range cases {

View File

@ -167,7 +167,6 @@ func (c *statsContext) Container() string {
}
func (c *statsContext) Name() string {
// TODO(thaJeztah): make this explicitly trim the "/" prefix, not just any char.
if len(c.s.Name) > 1 {
return c.s.Name[1:]
}

View File

@ -5,181 +5,45 @@ import (
"testing"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/test"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestContainerStatsContext(t *testing.T) {
const actorID = "c74518277ddc15a6afeaaeb06ee5f7433dcb27188224777c1efa7df1e8766d65"
containerID := test.RandomID()
var ctx statsContext
tests := []struct {
name string
tt := []struct {
stats StatsEntry
osType string
expValue string
expHeader string
call func() string
}{
{
name: "Container id",
stats: StatsEntry{ID: actorID, Container: actorID},
expValue: actorID,
expHeader: containerHeader,
call: ctx.Container,
},
{
name: "Container name",
stats: StatsEntry{ID: actorID, Container: "a-long-container-name"},
expValue: "a-long-container-name",
expHeader: containerHeader,
call: ctx.Container,
},
{
name: "ID",
stats: StatsEntry{ID: actorID},
expValue: actorID,
expHeader: formatter.ContainerIDHeader,
call: ctx.ID,
},
{
name: "Name",
stats: StatsEntry{Name: "/container-name"},
expValue: "container-name",
expHeader: formatter.ContainerIDHeader,
call: ctx.Name,
},
{
name: "Name empty",
stats: StatsEntry{Name: ""},
expValue: "--",
expHeader: formatter.ContainerIDHeader,
call: ctx.Name,
},
{
name: "Name prefix only",
stats: StatsEntry{Name: "/"},
expValue: "--",
expHeader: formatter.ContainerIDHeader,
call: ctx.Name,
},
{
name: "CPUPerc",
stats: StatsEntry{CPUPercentage: 5.5},
expValue: "5.50%",
expHeader: cpuPercHeader,
call: ctx.CPUPerc,
},
{
name: "CPUPerc invalid",
stats: StatsEntry{CPUPercentage: 5.5, IsInvalid: true},
expValue: "--",
expHeader: cpuPercHeader,
call: ctx.CPUPerc,
},
{
name: "NetIO",
stats: StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3},
expValue: "0.31B / 12.3B",
expHeader: netIOHeader,
call: ctx.NetIO,
},
{
name: "NetIO invalid",
stats: StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3, IsInvalid: true},
expValue: "--",
expHeader: netIOHeader,
call: ctx.NetIO,
},
{
name: "BlockIO",
stats: StatsEntry{BlockRead: 0.1, BlockWrite: 2.3},
expValue: "0.1B / 2.3B",
expHeader: blockIOHeader,
call: ctx.BlockIO,
},
{
name: "BlockIO invalid",
stats: StatsEntry{BlockRead: 0.1, BlockWrite: 2.3, IsInvalid: true},
expValue: "--",
expHeader: blockIOHeader,
call: ctx.BlockIO,
},
{
name: "MemPerc",
stats: StatsEntry{MemoryPercentage: 10.2},
expValue: "10.20%",
expHeader: memPercHeader,
call: ctx.MemPerc,
},
{
name: "MemPerc invalid",
stats: StatsEntry{MemoryPercentage: 10.2, IsInvalid: true},
expValue: "--",
expHeader: memPercHeader,
call: ctx.MemPerc,
},
{
name: "MemPerc windows",
stats: StatsEntry{MemoryPercentage: 10.2},
osType: "windows",
expValue: "--",
expHeader: memPercHeader,
call: ctx.MemPerc,
},
{
name: "MemUsage",
stats: StatsEntry{Memory: 24, MemoryLimit: 30},
expValue: "24B / 30B",
expHeader: memUseHeader,
call: ctx.MemUsage,
},
{
name: "MemUsage invalid",
stats: StatsEntry{Memory: 24, MemoryLimit: 30, IsInvalid: true},
expValue: "-- / --",
expHeader: memUseHeader,
call: ctx.MemUsage,
},
{
name: "MemUsage windows",
stats: StatsEntry{Memory: 24, MemoryLimit: 30},
osType: "windows",
expValue: "24B",
expHeader: winMemUseHeader,
call: ctx.MemUsage,
},
{
name: "PIDs",
stats: StatsEntry{PidsCurrent: 10},
expValue: "10",
expHeader: pidsHeader,
call: ctx.PIDs,
},
{
name: "PIDs invalid",
stats: StatsEntry{PidsCurrent: 10, IsInvalid: true},
expValue: "--",
expHeader: pidsHeader,
call: ctx.PIDs,
},
{
name: "PIDs windows",
stats: StatsEntry{PidsCurrent: 10},
osType: "windows",
expValue: "--",
expHeader: pidsHeader,
call: ctx.PIDs,
},
{StatsEntry{Container: containerID}, "", containerID, containerHeader, ctx.Container},
{StatsEntry{CPUPercentage: 5.5}, "", "5.50%", cpuPercHeader, ctx.CPUPerc},
{StatsEntry{CPUPercentage: 5.5, IsInvalid: true}, "", "--", cpuPercHeader, ctx.CPUPerc},
{StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3}, "", "0.31B / 12.3B", netIOHeader, ctx.NetIO},
{StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3, IsInvalid: true}, "", "--", netIOHeader, ctx.NetIO},
{StatsEntry{BlockRead: 0.1, BlockWrite: 2.3}, "", "0.1B / 2.3B", blockIOHeader, ctx.BlockIO},
{StatsEntry{BlockRead: 0.1, BlockWrite: 2.3, IsInvalid: true}, "", "--", blockIOHeader, ctx.BlockIO},
{StatsEntry{MemoryPercentage: 10.2}, "", "10.20%", memPercHeader, ctx.MemPerc},
{StatsEntry{MemoryPercentage: 10.2, IsInvalid: true}, "", "--", memPercHeader, ctx.MemPerc},
{StatsEntry{MemoryPercentage: 10.2}, "windows", "--", memPercHeader, ctx.MemPerc},
{StatsEntry{Memory: 24, MemoryLimit: 30}, "", "24B / 30B", memUseHeader, ctx.MemUsage},
{StatsEntry{Memory: 24, MemoryLimit: 30, IsInvalid: true}, "", "-- / --", memUseHeader, ctx.MemUsage},
{StatsEntry{Memory: 24, MemoryLimit: 30}, "windows", "24B", winMemUseHeader, ctx.MemUsage},
{StatsEntry{PidsCurrent: 10}, "", "10", pidsHeader, ctx.PIDs},
{StatsEntry{PidsCurrent: 10, IsInvalid: true}, "", "--", pidsHeader, ctx.PIDs},
{StatsEntry{PidsCurrent: 10}, "windows", "--", pidsHeader, ctx.PIDs},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctx = statsContext{s: tc.stats, os: tc.osType}
if v := tc.call(); v != tc.expValue {
t.Fatalf("Expected %q, got %q", tc.expValue, v)
}
})
for _, te := range tt {
ctx = statsContext{s: te.stats, os: te.osType}
if v := te.call(); v != te.expValue {
t.Fatalf("Expected %q, got %q", te.expValue, v)
}
}
}

View File

@ -8,8 +8,8 @@ import (
"sync"
"github.com/docker/cli/cli/command"
"github.com/moby/moby/api/pkg/stdcopy"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stdcopy"
"github.com/moby/term"
"github.com/sirupsen/logrus"
)
@ -38,7 +38,7 @@ type hijackedIOStreamer struct {
outputStream io.Writer
errorStream io.Writer
resp client.HijackedResponse
resp types.HijackedResponse
tty bool
detachKeys string

View File

@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.23
package container
@ -11,7 +11,6 @@ import (
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/inspect"
flagsHelper "github.com/docker/cli/cli/flags"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
@ -22,7 +21,7 @@ type inspectOptions struct {
}
// newInspectCommand creates a new cobra.Command for `docker container inspect`
func newInspectCommand(dockerCLI command.Cli) *cobra.Command {
func newInspectCommand(dockerCli command.Cli) *cobra.Command {
var opts inspectOptions
cmd := &cobra.Command{
@ -31,10 +30,9 @@ func newInspectCommand(dockerCLI command.Cli) *cobra.Command {
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.refs = args
return runInspect(cmd.Context(), dockerCLI, opts)
return runInspect(cmd.Context(), dockerCli, opts)
},
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
DisableFlagsInUseLine: true,
ValidArgsFunction: completion.ContainerNames(dockerCli, true),
}
flags := cmd.Flags()
@ -47,10 +45,6 @@ func newInspectCommand(dockerCLI command.Cli) *cobra.Command {
func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error {
apiClient := dockerCLI.Client()
return inspect.Inspect(dockerCLI.Out(), opts.refs, opts.format, func(ref string) (any, []byte, error) {
res, err := apiClient.ContainerInspect(ctx, ref, client.ContainerInspectOptions{Size: opts.size})
if err != nil {
return nil, nil, err
}
return &res.Container, res.Raw, nil
return apiClient.ContainerInspectWithRaw(ctx, ref, opts.size)
})
}

View File

@ -8,7 +8,6 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
@ -18,7 +17,13 @@ type killOptions struct {
containers []string
}
// newKillCommand creates a new cobra.Command for "docker container kill"
// NewKillCommand creates a new cobra.Command for `docker kill`
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewKillCommand(dockerCLI command.Cli) *cobra.Command {
return newKillCommand(dockerCLI)
}
func newKillCommand(dockerCLI command.Cli) *cobra.Command {
var opts killOptions
@ -33,8 +38,7 @@ func newKillCommand(dockerCLI command.Cli) *cobra.Command {
Annotations: map[string]string{
"aliases": "docker container kill, docker kill",
},
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
DisableFlagsInUseLine: true,
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
}
flags := cmd.Flags()
@ -48,10 +52,7 @@ func newKillCommand(dockerCLI command.Cli) *cobra.Command {
func runKill(ctx context.Context, dockerCLI command.Cli, opts *killOptions) error {
apiClient := dockerCLI.Client()
errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error {
_, err := apiClient.ContainerKill(ctx, container, client.ContainerKillOptions{
Signal: opts.signal,
})
return err
return apiClient.ContainerKill(ctx, container, opts.signal)
})
var errs []error

View File

@ -8,16 +8,19 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestRunKill(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerKillFunc: func(ctx context.Context, container string, options client.ContainerKillOptions) (client.ContainerKillResult, error) {
assert.Assert(t, is.Equal(options.Signal, "STOP"))
return client.ContainerKillResult{}, nil
containerKillFunc: func(
ctx context.Context,
container string,
signal string,
) error {
assert.Assert(t, is.Equal(signal, "STOP"))
return nil
},
})
@ -44,8 +47,12 @@ func TestRunKill(t *testing.T) {
func TestRunKillClientError(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerKillFunc: func(ctx context.Context, container string, options client.ContainerKillOptions) (client.ContainerKillResult, error) {
return client.ContainerKillResult{}, fmt.Errorf("client error for container %s", container)
containerKillFunc: func(
ctx context.Context,
container string,
signal string,
) error {
return fmt.Errorf("client error for container %s", container)
},
})

View File

@ -2,7 +2,6 @@ package container
import (
"context"
"fmt"
"io"
"github.com/docker/cli/cli"
@ -11,7 +10,8 @@ import (
flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/cli/opts"
"github.com/docker/cli/templates"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -27,7 +27,13 @@ type psOptions struct {
filter opts.FilterOpt
}
// newPsCommand creates a new cobra.Command for "docker container ps"
// NewPsCommand creates a new cobra.Command for `docker ps`
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewPsCommand(dockerCLI command.Cli) *cobra.Command {
return newPsCommand(dockerCLI)
}
func newPsCommand(dockerCLI command.Cli) *cobra.Command {
options := psOptions{filter: opts.NewFilterOpt()}
@ -43,8 +49,7 @@ func newPsCommand(dockerCLI command.Cli) *cobra.Command {
"category-top": "3",
"aliases": "docker container ls, docker container list, docker container ps, docker ps",
},
ValidArgsFunction: cobra.NoFileCompletions,
DisableFlagsInUseLine: true,
ValidArgsFunction: cobra.NoFileCompletions,
}
flags := cmd.Flags()
@ -68,8 +73,8 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command {
return &cmd
}
func buildContainerListOptions(options *psOptions) (client.ContainerListOptions, error) {
listOptions := client.ContainerListOptions{
func buildContainerListOptions(options *psOptions) (*container.ListOptions, error) {
listOptions := &container.ListOptions{
All: options.all,
Limit: options.last,
Size: options.size,
@ -82,9 +87,9 @@ func buildContainerListOptions(options *psOptions) (client.ContainerListOptions,
// always validate template when `--format` is used, for consistency
if len(options.format) > 0 {
tmpl, err := templates.Parse(options.format)
tmpl, err := templates.NewParse("", options.format)
if err != nil {
return client.ContainerListOptions{}, fmt.Errorf("failed to parse template: %w", err)
return nil, errors.Wrap(err, "failed to parse template")
}
optionsProcessor := formatter.NewContainerContext()
@ -92,7 +97,7 @@ func buildContainerListOptions(options *psOptions) (client.ContainerListOptions,
// This shouldn't error out but swallowing the error makes it harder
// to track down if preProcessor issues come up.
if err := tmpl.Execute(io.Discard, optionsProcessor); err != nil {
return client.ContainerListOptions{}, fmt.Errorf("failed to execute template: %w", err)
return nil, errors.Wrap(err, "failed to execute template")
}
// if `size` was not explicitly set to false (with `--size=false`)
@ -127,7 +132,7 @@ func runPs(ctx context.Context, dockerCLI command.Cli, options *psOptions) error
return err
}
res, err := dockerCLI.Client().ContainerList(ctx, listOptions)
containers, err := dockerCLI.Client().ContainerList(ctx, *listOptions)
if err != nil {
return err
}
@ -137,5 +142,5 @@ func runPs(ctx context.Context, dockerCLI command.Cli, options *psOptions) error
Format: formatter.NewContainerFormat(options.format, options.quiet, listOptions.Size),
Trunc: !options.noTrunc,
}
return formatter.ContainerWrite(containerCtx, res.Items)
return formatter.ContainerWrite(containerCtx, containers)
}

View File

@ -9,8 +9,7 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/builders"
"github.com/docker/cli/opts"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
@ -26,7 +25,7 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
expectedAll bool
expectedSize bool
expectedLimit int
expectedFilters client.Filters
expectedFilters map[string]string
}{
{
psOpts: &psOptions{
@ -35,10 +34,13 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
last: 5,
filter: filters,
},
expectedAll: true,
expectedSize: true,
expectedLimit: 5,
expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"),
expectedAll: true,
expectedSize: true,
expectedLimit: 5,
expectedFilters: map[string]string{
"foo": "bar",
"baz": "foo",
},
},
{
psOpts: &psOptions{
@ -47,9 +49,10 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
last: -1,
nLatest: true,
},
expectedAll: true,
expectedSize: true,
expectedLimit: 1,
expectedAll: true,
expectedSize: true,
expectedLimit: 1,
expectedFilters: make(map[string]string),
},
{
psOpts: &psOptions{
@ -60,10 +63,13 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
// With .Size, size should be true
format: "{{.Size}}",
},
expectedAll: true,
expectedSize: true,
expectedLimit: 5,
expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"),
expectedAll: true,
expectedSize: true,
expectedLimit: 5,
expectedFilters: map[string]string{
"foo": "bar",
"baz": "foo",
},
},
{
psOpts: &psOptions{
@ -74,10 +80,13 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
// With .Size, size should be true
format: "{{.Size}} {{.CreatedAt}} {{upper .Networks}}",
},
expectedAll: true,
expectedSize: true,
expectedLimit: 5,
expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"),
expectedAll: true,
expectedSize: true,
expectedLimit: 5,
expectedFilters: map[string]string{
"foo": "bar",
"baz": "foo",
},
},
{
psOpts: &psOptions{
@ -88,10 +97,13 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
// Without .Size, size should be false
format: "{{.CreatedAt}} {{.Networks}}",
},
expectedAll: true,
expectedSize: false,
expectedLimit: 5,
expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"),
expectedAll: true,
expectedSize: false,
expectedLimit: 5,
expectedFilters: map[string]string{
"foo": "bar",
"baz": "foo",
},
},
}
@ -102,14 +114,21 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
assert.Check(t, is.Equal(c.expectedAll, options.All))
assert.Check(t, is.Equal(c.expectedSize, options.Size))
assert.Check(t, is.Equal(c.expectedLimit, options.Limit))
assert.Check(t, is.DeepEqual(c.expectedFilters, options.Filters))
assert.Check(t, is.Equal(len(c.expectedFilters), options.Filters.Len()))
for k, v := range c.expectedFilters {
f := options.Filters
if !f.ExactMatch(k, v) {
t.Fatalf("Expected filter with key %s to be %s but got %s", k, v, f.Get(k))
}
}
}
}
func TestContainerListErrors(t *testing.T) {
testCases := []struct {
flags map[string]string
containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error)
containerListFunc func(container.ListOptions) ([]container.Summary, error)
expectedError string
}{
{
@ -125,8 +144,8 @@ func TestContainerListErrors(t *testing.T) {
expectedError: `wrong number of args for join`,
},
{
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
return client.ContainerListResult{}, errors.New("error listing containers")
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
return nil, errors.New("error listing containers")
},
expectedError: "error listing containers",
},
@ -149,15 +168,13 @@ func TestContainerListErrors(t *testing.T) {
func TestContainerListWithoutFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
return client.ContainerListResult{
Items: []container.Summary{
*builders.Container("c1"),
*builders.Container("c2", builders.WithName("foo")),
*builders.Container("c3", builders.WithPort(80, 80, builders.TCP), builders.WithPort(81, 81, builders.TCP), builders.WithPort(82, 82, builders.TCP)),
*builders.Container("c4", builders.WithPort(81, 81, builders.UDP)),
*builders.Container("c5", builders.WithPort(82, 82, builders.IP("8.8.8.8"), builders.TCP)),
},
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
return []container.Summary{
*builders.Container("c1"),
*builders.Container("c2", builders.WithName("foo")),
*builders.Container("c3", builders.WithPort(80, 80, builders.TCP), builders.WithPort(81, 81, builders.TCP), builders.WithPort(82, 82, builders.TCP)),
*builders.Container("c4", builders.WithPort(81, 81, builders.UDP)),
*builders.Container("c5", builders.WithPort(82, 82, builders.IP("8.8.8.8"), builders.TCP)),
}, nil
},
})
@ -171,12 +188,10 @@ func TestContainerListWithoutFormat(t *testing.T) {
func TestContainerListNoTrunc(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
return client.ContainerListResult{
Items: []container.Summary{
*builders.Container("c1"),
*builders.Container("c2", builders.WithName("foo/bar")),
},
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
return []container.Summary{
*builders.Container("c1"),
*builders.Container("c2", builders.WithName("foo/bar")),
}, nil
},
})
@ -192,12 +207,10 @@ func TestContainerListNoTrunc(t *testing.T) {
// Test for GitHub issue docker/docker#21772
func TestContainerListNamesMultipleTime(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
return client.ContainerListResult{
Items: []container.Summary{
*builders.Container("c1"),
*builders.Container("c2", builders.WithName("foo/bar")),
},
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
return []container.Summary{
*builders.Container("c1"),
*builders.Container("c2", builders.WithName("foo/bar")),
}, nil
},
})
@ -213,12 +226,10 @@ func TestContainerListNamesMultipleTime(t *testing.T) {
// Test for GitHub issue docker/docker#30291
func TestContainerListFormatTemplateWithArg(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
return client.ContainerListResult{
Items: []container.Summary{
*builders.Container("c1", builders.WithLabel("some.label", "value")),
*builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar")),
},
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
return []container.Summary{
*builders.Container("c1", builders.WithLabel("some.label", "value")),
*builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar")),
}, nil
},
})
@ -268,9 +279,9 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) {
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(options client.ContainerListOptions) (client.ContainerListResult, error) {
containerListFunc: func(options container.ListOptions) ([]container.Summary, error) {
assert.Check(t, is.Equal(options.Size, tc.sizeExpected))
return client.ContainerListResult{}, nil
return []container.Summary{}, nil
},
})
cmd := newListCommand(cli)
@ -288,12 +299,10 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) {
func TestContainerListWithConfigFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
return client.ContainerListResult{
Items: []container.Summary{
*builders.Container("c1", builders.WithLabel("some.label", "value"), builders.WithSize(10700000)),
*builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar"), builders.WithSize(3200000)),
},
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
return []container.Summary{
*builders.Container("c1", builders.WithLabel("some.label", "value"), builders.WithSize(10700000)),
*builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar"), builders.WithSize(3200000)),
}, nil
},
})
@ -310,12 +319,10 @@ func TestContainerListWithConfigFormat(t *testing.T) {
func TestContainerListWithFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
return client.ContainerListResult{
Items: []container.Summary{
*builders.Container("c1", builders.WithLabel("some.label", "value")),
*builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar")),
},
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
return []container.Summary{
*builders.Container("c1", builders.WithLabel("some.label", "value")),
*builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar")),
}, nil
},
})

View File

@ -7,8 +7,8 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/moby/moby/api/pkg/stdcopy"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stdcopy"
"github.com/spf13/cobra"
)
@ -23,7 +23,13 @@ type logsOptions struct {
container string
}
// newLogsCommand creates a new cobra.Command for "docker container logs"
// NewLogsCommand creates a new cobra.Command for `docker logs`
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewLogsCommand(dockerCLI command.Cli) *cobra.Command {
return newLogsCommand(dockerCLI)
}
func newLogsCommand(dockerCLI command.Cli) *cobra.Command {
var opts logsOptions
@ -38,8 +44,7 @@ func newLogsCommand(dockerCLI command.Cli) *cobra.Command {
Annotations: map[string]string{
"aliases": "docker container logs, docker logs",
},
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
DisableFlagsInUseLine: true,
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
}
flags := cmd.Flags()
@ -54,12 +59,12 @@ func newLogsCommand(dockerCLI command.Cli) *cobra.Command {
}
func runLogs(ctx context.Context, dockerCli command.Cli, opts *logsOptions) error {
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container, client.ContainerInspectOptions{})
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container)
if err != nil {
return err
}
resp, err := dockerCli.Client().ContainerLogs(ctx, c.Container.ID, client.ContainerLogsOptions{
responseBody, err := dockerCli.Client().ContainerLogs(ctx, c.ID, container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Since: opts.since,
@ -72,12 +77,12 @@ func runLogs(ctx context.Context, dockerCli command.Cli, opts *logsOptions) erro
if err != nil {
return err
}
defer func() { _ = resp.Close() }()
defer responseBody.Close()
if c.Container.Config.Tty {
_, err = io.Copy(dockerCli.Out(), resp)
if c.Config.Tty {
_, err = io.Copy(dockerCli.Out(), responseBody)
} else {
_, err = stdcopy.StdCopy(dockerCli.Out(), dockerCli.Err(), resp)
_, err = stdcopy.StdCopy(dockerCli.Out(), dockerCli.Err(), responseBody)
}
return err
}

View File

@ -2,22 +2,27 @@ package container
import (
"context"
"io"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
var logFn = func(expectedOut string) func(string, container.LogsOptions) (io.ReadCloser, error) {
return func(container string, opts container.LogsOptions) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader(expectedOut)), nil
}
}
func TestRunLogs(t *testing.T) {
inspectFn := func(containerID string) (client.ContainerInspectResult, error) {
return client.ContainerInspectResult{
Container: container.InspectResponse{
Config: &container.Config{Tty: true},
State: &container.State{Running: false},
},
inspectFn := func(containerID string) (container.InspectResponse, error) {
return container.InspectResponse{
Config: &container.Config{Tty: true},
ContainerJSONBase: &container.ContainerJSONBase{State: &container.State{Running: false}},
}, nil
}
@ -33,13 +38,7 @@ func TestRunLogs(t *testing.T) {
doc: "successful logs",
expectedOut: "foo",
options: &logsOptions{},
client: &fakeClient{
logFunc: func(container string, opts client.ContainerLogsOptions) (client.ContainerLogsResult, error) {
// FIXME(thaJeztah): how to mock this?
return mockContainerLogsResult("foo"), nil
},
inspectFunc: inspectFn,
},
client: &fakeClient{logFunc: logFn("foo"), inspectFunc: inspectFn},
},
}

View File

@ -1,30 +1,25 @@
// FIXME(vvoland): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
package container
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net"
"net/netip"
"os"
"path"
"path/filepath"
"reflect"
"slices"
"strconv"
"strings"
"time"
"github.com/docker/cli/internal/lazyregexp"
"github.com/docker/cli/internal/volumespec"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types/container"
mounttypes "github.com/docker/docker/api/types/mount"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/mount"
"github.com/moby/moby/api/types/network"
"github.com/pkg/errors"
"github.com/spf13/pflag"
cdi "tags.cncf.io/container-device-interface/pkg/parser"
)
@ -56,7 +51,7 @@ type containerOptions struct {
deviceWriteBps opts.ThrottledeviceOpt
links opts.ListOpts
aliases opts.ListOpts
linkLocalIPs opts.ListOpts // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly
linkLocalIPs opts.ListOpts
deviceReadIOps opts.ThrottledeviceOpt
deviceWriteIOps opts.ThrottledeviceOpt
env opts.ListOpts
@ -68,7 +63,7 @@ type containerOptions struct {
sysctls *opts.MapOpts
publish opts.ListOpts
expose opts.ListOpts
dns opts.ListOpts // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly
dns opts.ListOpts
dnsSearch opts.ListOpts
dnsOptions opts.ListOpts
extraHosts opts.ListOpts
@ -98,6 +93,7 @@ type containerOptions struct {
memory opts.MemBytes
memoryReservation opts.MemBytes
memorySwap opts.MemSwapBytes
kernelMemory opts.MemBytes
user string
workingDir string
cpuCount int64
@ -116,8 +112,8 @@ type containerOptions struct {
swappiness int64
netMode opts.NetworkOpt
macAddress string
ipv4Address net.IP // TODO(thaJeztah): we need a flag-type to handle netip.Addr directly
ipv6Address net.IP // TODO(thaJeztah): we need a flag-type to handle netip.Addr directly
ipv4Address string
ipv6Address string
ipcMode string
pidsLimit int64
restartPolicy string
@ -145,6 +141,16 @@ type containerOptions struct {
Args []string
}
// addPlatformFlag adds "--platform" to a set of flags for API version 1.32 and
// later, using the value of "DOCKER_DEFAULT_PLATFORM" (if set) as a default.
//
// It should not be used for new uses, which may have a different API version
// requirement.
func addPlatformFlag(flags *pflag.FlagSet, target *string) {
flags.StringVar(target, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
}
// addFlags adds all command line flags that will be used by parse to the FlagSet
func addFlags(flags *pflag.FlagSet) *containerOptions {
copts := &containerOptions{
@ -233,8 +239,8 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
flags.MarkHidden("dns-opt")
flags.Var(&copts.dnsSearch, "dns-search", "Set custom DNS search domains")
flags.Var(&copts.expose, "expose", "Expose a port or a range of ports")
flags.IPVar(&copts.ipv4Address, "ip", nil, "IPv4 address (e.g., 172.30.100.104)")
flags.IPVar(&copts.ipv6Address, "ip6", nil, "IPv6 address (e.g., 2001:db8::33)")
flags.StringVar(&copts.ipv4Address, "ip", "", "IPv4 address (e.g., 172.30.100.104)")
flags.StringVar(&copts.ipv6Address, "ip6", "", "IPv6 address (e.g., 2001:db8::33)")
flags.Var(&copts.links, "link", "Add link to another container")
flags.Var(&copts.linkLocalIPs, "link-local-ip", "Container IPv4/IPv6 link-local addresses")
flags.StringVar(&copts.macAddress, "mac-address", "", "Container MAC address (e.g., 92:d0:c6:0a:29:33)")
@ -297,6 +303,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
flags.SetAnnotation("io-maxbandwidth", "ostype", []string{"windows"})
flags.Uint64Var(&copts.ioMaxIOps, "io-maxiops", 0, "Maximum IOps limit for the system drive (Windows only)")
flags.SetAnnotation("io-maxiops", "ostype", []string{"windows"})
flags.Var(&copts.kernelMemory, "kernel-memory", "Kernel memory limit")
flags.VarP(&copts.memory, "memory", "m", "Memory limit")
flags.Var(&copts.memoryReservation, "memory-reservation", "Memory soft limit")
flags.Var(&copts.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap")
@ -320,18 +327,13 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
flags.Var(copts.annotations, "annotation", "Add an annotation to the container (passed through to the OCI runtime)")
flags.SetAnnotation("annotation", "version", []string{"1.43"})
// TODO(thaJeztah): remove in next release (v30.0, or v29.x)
var stub opts.MemBytes
flags.Var(&stub, "kernel-memory", "Kernel memory limit (deprecated)")
_ = flags.MarkDeprecated("kernel-memory", "and no longer supported by the kernel")
return copts
}
type containerConfig struct {
Config *container.Config
HostConfig *container.HostConfig
NetworkingConfig *network.NetworkingConfig
NetworkingConfig *networktypes.NetworkingConfig
}
// parse parses the args for the specified command and generates a Config,
@ -348,8 +350,8 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
// Validate the input mac address
if copts.macAddress != "" {
if _, err := net.ParseMAC(strings.TrimSpace(copts.macAddress)); err != nil {
return nil, fmt.Errorf("%s is not a valid mac address", copts.macAddress)
if _, err := opts.ValidateMACAddress(copts.macAddress); err != nil {
return nil, errors.Errorf("%s is not a valid mac address", copts.macAddress)
}
}
if copts.stdin {
@ -365,7 +367,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
swappiness := copts.swappiness
if swappiness != -1 && (swappiness < 0 || swappiness > 100) {
return nil, fmt.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness)
return nil, errors.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness)
}
var binds []string
@ -380,7 +382,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
if parsed.Source != "" {
toBind := bind
if parsed.Type == string(mount.TypeBind) {
if parsed.Type == string(mounttypes.TypeBind) {
if hostPart, targetPath, ok := strings.Cut(bind, ":"); ok {
if !filepath.IsAbs(hostPart) && strings.HasPrefix(hostPart, ".") {
if absHostPart, err := filepath.Abs(hostPart); err == nil {
@ -420,60 +422,45 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
entrypoint = []string{""}
}
// TODO(thaJeztah): remove uses of go-connections/nat here.
convertedOpts, err := convertToStandardNotation(copts.publish.GetSlice())
publishOpts := copts.publish.GetSlice()
var (
ports map[nat.Port]struct{}
portBindings map[nat.Port][]nat.PortBinding
convertedOpts []string
)
convertedOpts, err = convertToStandardNotation(publishOpts)
if err != nil {
return nil, err
}
ports, natPortBindings, err := nat.ParsePortSpecs(convertedOpts)
ports, portBindings, err = nat.ParsePortSpecs(convertedOpts)
if err != nil {
return nil, err
}
portBindings := network.PortMap{}
for port, bindings := range natPortBindings {
p, err := network.ParsePort(string(port))
if err != nil {
return nil, err
}
portBindings[p] = []network.PortBinding{}
for _, b := range bindings {
var hostIP netip.Addr
if b.HostIP != "" {
hostIP, err = netip.ParseAddr(b.HostIP)
if err != nil {
return nil, err
}
}
portBindings[p] = append(portBindings[p], network.PortBinding{
HostIP: hostIP,
HostPort: b.HostPort,
})
}
}
// Add published ports as exposed ports.
exposedPorts := network.PortSet{}
for port := range ports {
p, err := network.ParsePort(string(port))
if err != nil {
return nil, err
}
exposedPorts[p] = struct{}{}
}
// Merge in exposed ports to the map of published ports
for _, e := range copts.expose.GetSlice() {
if strings.Contains(e, ":") {
return nil, errors.Errorf("invalid port format for --expose: %s", e)
}
// support two formats for expose, original format <portnum>/[<proto>]
// or <startport-endport>/[<proto>]
pr, err := network.ParsePortRange(e)
if err != nil {
return nil, fmt.Errorf("invalid range format for --expose: %w", err)
}
proto, port := nat.SplitProtoPort(e)
// parse the start and end port and create a sequence of ports to expose
// if expose a port, the start and end port are the same
for p := range pr.All() {
exposedPorts[p] = struct{}{}
start, end, err := nat.ParsePortRange(port)
if err != nil {
return nil, errors.Errorf("invalid range format for --expose: %s, error: %s", e, err)
}
for i := start; i <= end; i++ {
p, err := nat.NewPort(proto, strconv.FormatUint(i, 10))
if err != nil {
return nil, err
}
if _, exists := ports[p]; !exists {
ports[p] = struct{}{}
}
}
}
@ -481,19 +468,23 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
// device path (as opposed to during flag parsing), as at the time we are
// parsing flags, we haven't yet sent a _ping to the daemon to determine
// what operating system it is.
devices := copts.devices.GetSlice()
deviceMappings := make([]container.DeviceMapping, 0, len(devices))
cdiDeviceNames := make([]string, 0, len(devices))
for _, device := range devices {
deviceMappings := []container.DeviceMapping{}
var cdiDeviceNames []string
for _, device := range copts.devices.GetSlice() {
var (
validated string
deviceMapping container.DeviceMapping
err error
)
if cdi.IsQualifiedName(device) {
cdiDeviceNames = append(cdiDeviceNames, device)
continue
}
validated, err := validateDevice(device, serverOS)
validated, err = validateDevice(device, serverOS)
if err != nil {
return nil, err
}
deviceMapping, err := parseDevice(validated, serverOS)
deviceMapping, err = parseDevice(validated, serverOS)
if err != nil {
return nil, err
}
@ -514,22 +505,22 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
pidMode := container.PidMode(copts.pidMode)
if !pidMode.Valid() {
return nil, errors.New("--pid: invalid PID mode")
return nil, errors.Errorf("--pid: invalid PID mode")
}
utsMode := container.UTSMode(copts.utsMode)
if !utsMode.Valid() {
return nil, errors.New("--uts: invalid UTS mode")
return nil, errors.Errorf("--uts: invalid UTS mode")
}
usernsMode := container.UsernsMode(copts.usernsMode)
if !usernsMode.Valid() {
return nil, errors.New("--userns: invalid USER mode")
return nil, errors.Errorf("--userns: invalid USER mode")
}
cgroupnsMode := container.CgroupnsMode(copts.cgroupnsMode)
if !cgroupnsMode.Valid() {
return nil, errors.New("--cgroupns: invalid CGROUP mode")
return nil, errors.Errorf("--cgroupns: invalid CGROUP mode")
}
restartPolicy, err := opts.ParseRestartPolicy(copts.restartPolicy)
@ -564,7 +555,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
copts.healthStartInterval != 0
if copts.noHealthcheck {
if haveHealthSettings {
return nil, errors.New("--no-healthcheck conflicts with --health-* options")
return nil, errors.Errorf("--no-healthcheck conflicts with --health-* options")
}
healthConfig = &container.HealthConfig{Test: []string{"NONE"}}
} else if haveHealthSettings {
@ -573,13 +564,13 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
probe = []string{"CMD-SHELL", copts.healthCmd}
}
if copts.healthInterval < 0 {
return nil, errors.New("--health-interval cannot be negative")
return nil, errors.Errorf("--health-interval cannot be negative")
}
if copts.healthTimeout < 0 {
return nil, errors.New("--health-timeout cannot be negative")
return nil, errors.Errorf("--health-timeout cannot be negative")
}
if copts.healthRetries < 0 {
return nil, errors.New("--health-retries cannot be negative")
return nil, errors.Errorf("--health-retries cannot be negative")
}
if copts.healthStartPeriod < 0 {
return nil, errors.New("--health-start-period cannot be negative")
@ -613,6 +604,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
MemoryReservation: copts.memoryReservation.Value(),
MemorySwap: copts.memorySwap.Value(),
MemorySwappiness: &copts.swappiness,
KernelMemory: copts.kernelMemory.Value(),
OomKillDisable: &copts.oomKillDisable,
NanoCPUs: copts.cpus.Value(),
CPUCount: copts.cpuCount,
@ -642,7 +634,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
config := &container.Config{
Hostname: copts.hostname,
Domainname: copts.domainname,
ExposedPorts: exposedPorts,
ExposedPorts: ports,
User: copts.user,
Tty: copts.tty,
OpenStdin: copts.stdin,
@ -653,6 +645,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
Cmd: runCmd,
Image: copts.Image,
Volumes: volumes,
MacAddress: copts.macAddress,
Entrypoint: entrypoint,
WorkingDir: copts.workingDir,
Labels: opts.ConvertKVStringsToMap(labels),
@ -677,7 +670,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
// but pre created containers can still have those nil values.
// See https://github.com/docker/docker/pull/17779
// for a more detailed explanation on why we don't want that.
DNS: toNetipAddrSlice(copts.dns.GetAllOrEmpty()),
DNS: copts.dns.GetAllOrEmpty(),
DNSSearch: copts.dnsSearch.GetAllOrEmpty(),
DNSOptions: copts.dnsOptions.GetAllOrEmpty(),
ExtraHosts: copts.extraHosts.GetSlice(),
@ -710,7 +703,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
}
if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() {
return nil, errors.New("conflicting options: cannot specify both --restart and --rm")
return nil, errors.Errorf("conflicting options: cannot specify both --restart and --rm")
}
// only set this value if the user provided the flag, else it should default to nil
@ -723,17 +716,25 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
config.StdinOnce = true
}
epCfg, err := parseNetworkOpts(copts)
networkingConfig := &networktypes.NetworkingConfig{
EndpointsConfig: make(map[string]*networktypes.EndpointSettings),
}
networkingConfig.EndpointsConfig, err = parseNetworkOpts(copts)
if err != nil {
return nil, err
}
// Put the endpoint-specific MacAddress of the "main" network attachment into the container Config for backward
// compatibility with older daemons.
if nw, ok := networkingConfig.EndpointsConfig[hostConfig.NetworkMode.NetworkName()]; ok {
config.MacAddress = nw.MacAddress //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
}
return &containerConfig{
Config: config,
HostConfig: hostConfig,
NetworkingConfig: &network.NetworkingConfig{
EndpointsConfig: epCfg,
},
Config: config,
HostConfig: hostConfig,
NetworkingConfig: networkingConfig,
}, nil
}
@ -744,9 +745,9 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
// this function may return _multiple_ endpoints, which is not currently supported
// by the daemon, but may be in future; it's up to the daemon to produce an error
// in case that is not supported.
func parseNetworkOpts(copts *containerOptions) (map[string]*network.EndpointSettings, error) {
func parseNetworkOpts(copts *containerOptions) (map[string]*networktypes.EndpointSettings, error) {
var (
endpoints = make(map[string]*network.EndpointSettings, len(copts.netMode.Value()))
endpoints = make(map[string]*networktypes.EndpointSettings, len(copts.netMode.Value()))
hasUserDefined, hasNonUserDefined bool
)
@ -785,14 +786,14 @@ func parseNetworkOpts(copts *containerOptions) (map[string]*network.EndpointSett
return nil, err
}
if _, ok := endpoints[n.Target]; ok {
return nil, invalidParameter(fmt.Errorf("network %q is specified multiple times", n.Target))
return nil, invalidParameter(errors.Errorf("network %q is specified multiple times", n.Target))
}
// For backward compatibility: if no custom options are provided for the network,
// and only a single network is specified, omit the endpoint-configuration
// on the client (the daemon will still create it when creating the container)
if i == 0 && len(copts.netMode.Value()) == 1 {
if ep == nil || reflect.DeepEqual(*ep, network.EndpointSettings{}) {
if ep == nil || reflect.DeepEqual(*ep, networktypes.EndpointSettings{}) {
continue
}
}
@ -812,10 +813,10 @@ func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOption
if len(n.Links) > 0 && copts.links.Len() > 0 {
return invalidParameter(errors.New("conflicting options: cannot specify both --link and per-network links"))
}
if n.IPv4Address.IsValid() && copts.ipv4Address != nil {
if n.IPv4Address != "" && copts.ipv4Address != "" {
return invalidParameter(errors.New("conflicting options: cannot specify both --ip and per-network IPv4 address"))
}
if n.IPv6Address.IsValid() && copts.ipv6Address != nil {
if n.IPv6Address != "" && copts.ipv6Address != "" {
return invalidParameter(errors.New("conflicting options: cannot specify both --ip6 and per-network IPv6 address"))
}
if n.MacAddress != "" && copts.macAddress != "" {
@ -834,26 +835,23 @@ func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOption
n.Links = make([]string, copts.links.Len())
copy(n.Links, copts.links.GetSlice())
}
if copts.ipv4Address != nil {
if ipv4, ok := netip.AddrFromSlice(copts.ipv4Address.To4()); ok {
n.IPv4Address = ipv4
}
if copts.ipv4Address != "" {
n.IPv4Address = copts.ipv4Address
}
if copts.ipv6Address != nil {
if ipv6, ok := netip.AddrFromSlice(copts.ipv6Address.To16()); ok {
n.IPv6Address = ipv6
}
if copts.ipv6Address != "" {
n.IPv6Address = copts.ipv6Address
}
if copts.macAddress != "" {
n.MacAddress = copts.macAddress
}
if copts.linkLocalIPs.Len() > 0 {
n.LinkLocalIPs = toNetipAddrSlice(copts.linkLocalIPs.GetSlice())
n.LinkLocalIPs = make([]string, copts.linkLocalIPs.Len())
copy(n.LinkLocalIPs, copts.linkLocalIPs.GetSlice())
}
return nil
}
func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*network.EndpointSettings, error) {
func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*networktypes.EndpointSettings, error) {
if strings.TrimSpace(ep.Target) == "" {
return nil, errors.New("no name set for network")
}
@ -866,7 +864,7 @@ func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*network.Endpoint
}
}
epConfig := &network.EndpointSettings{
epConfig := &networktypes.EndpointSettings{
GwPriority: ep.GwPriority,
}
epConfig.Aliases = append(epConfig.Aliases, ep.Aliases...)
@ -877,19 +875,18 @@ func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*network.Endpoint
if len(ep.Links) > 0 {
epConfig.Links = ep.Links
}
if ep.IPv4Address.IsValid() || ep.IPv6Address.IsValid() || len(ep.LinkLocalIPs) > 0 {
epConfig.IPAMConfig = &network.EndpointIPAMConfig{
if ep.IPv4Address != "" || ep.IPv6Address != "" || len(ep.LinkLocalIPs) > 0 {
epConfig.IPAMConfig = &networktypes.EndpointIPAMConfig{
IPv4Address: ep.IPv4Address,
IPv6Address: ep.IPv6Address,
LinkLocalIPs: ep.LinkLocalIPs,
}
}
if ep.MacAddress != "" {
ma, err := net.ParseMAC(strings.TrimSpace(ep.MacAddress))
if err != nil {
return nil, fmt.Errorf("%s is not a valid mac address", ep.MacAddress)
if _, err := opts.ValidateMACAddress(ep.MacAddress); err != nil {
return nil, errors.Errorf("%s is not a valid mac address", ep.MacAddress)
}
epConfig.MacAddress = network.HardwareAddr(ma)
epConfig.MacAddress = ep.MacAddress
}
return epConfig, nil
}
@ -902,7 +899,7 @@ func convertToStandardNotation(ports []string) ([]string, error) {
for _, param := range strings.Split(publish, ",") {
k, v, ok := strings.Cut(param, "=")
if !ok || k == "" {
return optsList, fmt.Errorf("invalid publish opts format (should be name=value but got '%s')", param)
return optsList, errors.Errorf("invalid publish opts format (should be name=value but got '%s')", param)
}
params[k] = v
}
@ -917,7 +914,7 @@ func convertToStandardNotation(ports []string) ([]string, error) {
func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) {
loggingOptsMap := opts.ConvertKVStringsToMap(loggingOpts)
if loggingDriver == "none" && len(loggingOpts) > 0 {
return map[string]string{}, fmt.Errorf("invalid logging opts for driver %s", loggingDriver)
return map[string]string{}, errors.Errorf("invalid logging opts for driver %s", loggingDriver)
}
return loggingOptsMap, nil
}
@ -931,7 +928,7 @@ func parseSecurityOpts(securityOpts []string) ([]string, error) {
}
if (!ok || v == "") && k != "no-new-privileges" {
// "no-new-privileges" is the only option that does not require a value.
return securityOpts, fmt.Errorf("invalid --security-opt: %q", opt)
return securityOpts, errors.Errorf("Invalid --security-opt: %q", opt)
}
if k == "seccomp" {
switch v {
@ -942,11 +939,11 @@ func parseSecurityOpts(securityOpts []string) ([]string, error) {
// content if it's valid JSON.
f, err := os.ReadFile(v)
if err != nil {
return securityOpts, fmt.Errorf("opening seccomp profile (%s) failed: %w", v, err)
return securityOpts, errors.Errorf("opening seccomp profile (%s) failed: %v", v, err)
}
b := bytes.NewBuffer(nil)
if err := json.Compact(b, f); err != nil {
return securityOpts, fmt.Errorf("compacting json for seccomp profile (%s) failed: %w", v, err)
return securityOpts, errors.Errorf("compacting json for seccomp profile (%s) failed: %v", v, err)
}
securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes())
}
@ -981,7 +978,7 @@ func parseStorageOpts(storageOpts []string) (map[string]string, error) {
for _, option := range storageOpts {
k, v, ok := strings.Cut(option, "=")
if !ok {
return nil, errors.New("invalid storage option")
return nil, errors.Errorf("invalid storage option")
}
m[k] = v
}
@ -996,7 +993,7 @@ func parseDevice(device, serverOS string) (container.DeviceMapping, error) {
case "windows":
return parseWindowsDevice(device)
}
return container.DeviceMapping{}, fmt.Errorf("unknown server OS: %s", serverOS)
return container.DeviceMapping{}, errors.Errorf("unknown server OS: %s", serverOS)
}
// parseLinuxDevice parses a device mapping string to a container.DeviceMapping struct
@ -1020,7 +1017,7 @@ func parseLinuxDevice(device string) (container.DeviceMapping, error) {
case 1:
src = arr[0]
default:
return container.DeviceMapping{}, fmt.Errorf("invalid device specification: %s", device)
return container.DeviceMapping{}, errors.Errorf("invalid device specification: %s", device)
}
if dst == "" {
@ -1050,7 +1047,7 @@ func validateDeviceCgroupRule(val string) (string, error) {
return val, nil
}
return val, fmt.Errorf("invalid device cgroup format '%s'", val)
return val, errors.Errorf("invalid device cgroup format '%s'", val)
}
// validDeviceMode checks if the mode for device is valid or not.
@ -1082,7 +1079,7 @@ func validateDevice(val string, serverOS string) (string, error) {
// Windows does validation entirely server-side
return val, nil
}
return "", fmt.Errorf("unknown server OS: %s", serverOS)
return "", errors.Errorf("unknown server OS: %s", serverOS)
}
// validateLinuxPath is the implementation of validateDevice knowing that the
@ -1097,12 +1094,12 @@ func validateLinuxPath(val string, validator func(string) bool) (string, error)
var mode string
if strings.Count(val, ":") > 2 {
return val, fmt.Errorf("bad format for path: %s", val)
return val, errors.Errorf("bad format for path: %s", val)
}
split := strings.SplitN(val, ":", 3)
if split[0] == "" {
return val, fmt.Errorf("bad format for path: %s", val)
return val, errors.Errorf("bad format for path: %s", val)
}
switch len(split) {
case 1:
@ -1121,13 +1118,13 @@ func validateLinuxPath(val string, validator func(string) bool) (string, error)
containerPath = split[1]
mode = split[2]
if isValid := validator(split[2]); !isValid {
return val, fmt.Errorf("bad mode specified: %s", mode)
return val, errors.Errorf("bad mode specified: %s", mode)
}
val = fmt.Sprintf("%s:%s:%s", split[0], containerPath, mode)
}
if !path.IsAbs(containerPath) {
return val, fmt.Errorf("%s is not an absolute path", containerPath)
return val, errors.Errorf("%s is not an absolute path", containerPath)
}
return val, nil
}
@ -1135,23 +1132,10 @@ func validateLinuxPath(val string, validator func(string) bool) (string, error)
// validateAttach validates that the specified string is a valid attach option.
func validateAttach(val string) (string, error) {
s := strings.ToLower(val)
if slices.Contains([]string{"stdin", "stdout", "stderr"}, s) {
return s, nil
}
return val, errors.New("valid streams are STDIN, STDOUT and STDERR")
}
func toNetipAddrSlice(ips []string) []netip.Addr {
if len(ips) == 0 {
return nil
}
netIPs := make([]netip.Addr, 0, len(ips))
for _, ip := range ips {
addr, err := netip.ParseAddr(ip)
if err != nil {
continue
for _, str := range []string{"stdin", "stdout", "stderr"} {
if s == str {
return s, nil
}
netIPs = append(netIPs, addr)
}
return netIPs
return val, errors.Errorf("valid streams are STDIN, STDOUT and STDERR")
}

View File

@ -4,31 +4,21 @@ import (
"errors"
"fmt"
"io"
"net"
"net/netip"
"os"
"runtime"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/container"
networktypes "github.com/moby/moby/api/types/network"
"github.com/docker/docker/api/types/container"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/go-connections/nat"
"github.com/spf13/pflag"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/skip"
)
func mustParseMAC(s string) networktypes.HardwareAddr {
mac, err := net.ParseMAC(s)
if err != nil {
panic(err)
}
return networktypes.HardwareAddr(mac)
}
func TestValidateAttach(t *testing.T) {
valid := []string{
"stdin",
@ -58,7 +48,7 @@ func parseRun(args []string) (*container.Config, *container.HostConfig, *network
if err := flags.Parse(args); err != nil {
return nil, nil, nil, err
}
// TODO(dnephin): fix tests to accept ContainerConfig; see https://github.com/moby/moby/pull/31621
// TODO: fix tests to accept ContainerConfig
containerCfg, err := parse(flags, copts, runtime.GOOS)
if err != nil {
return nil, nil, nil, err
@ -360,9 +350,13 @@ func TestParseWithMacAddress(t *testing.T) {
if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" {
t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err)
}
_, hostConfig, nwConfig := mustParse(t, validMacAddress)
config, hostConfig, nwConfig := mustParse(t, validMacAddress)
if config.MacAddress != "92:d0:c6:0a:29:33" { //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as container-wide MacAddress, got '%v'",
config.MacAddress) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
}
defaultNw := hostConfig.NetworkMode.NetworkName()
if nwConfig.EndpointsConfig[defaultNw].MacAddress.String() != "92:d0:c6:0a:29:33" {
if nwConfig.EndpointsConfig[defaultNw].MacAddress != "92:d0:c6:0a:29:33" {
t.Fatalf("Expected the default endpoint to have the MacAddress '92:d0:c6:0a:29:33' set, got '%v'", nwConfig.EndpointsConfig[defaultNw].MacAddress)
}
}
@ -435,55 +429,56 @@ func TestParseHostnameDomainname(t *testing.T) {
}
func TestParseWithExpose(t *testing.T) {
t.Run("invalid", func(t *testing.T) {
tests := map[string]string{
":": `invalid range format for --expose: invalid start port ':': invalid syntax`,
"8080:9090": `invalid range format for --expose: invalid start port '8080:9090': invalid syntax`,
"/tcp": `invalid range format for --expose: invalid start port '': value is empty`,
"/udp": `invalid range format for --expose: invalid start port '': value is empty`,
"NaN/tcp": `invalid range format for --expose: invalid start port 'NaN': invalid syntax`,
"NaN-NaN/tcp": `invalid range format for --expose: invalid start port 'NaN': invalid syntax`,
"8080-NaN/tcp": `invalid range format for --expose: invalid end port 'NaN': invalid syntax`,
"1234567890-8080/tcp": `invalid range format for --expose: invalid start port '1234567890': value out of range`,
invalids := map[string]string{
":": "invalid port format for --expose: :",
"8080:9090": "invalid port format for --expose: 8080:9090",
"/tcp": "invalid range format for --expose: /tcp, error: empty string specified for ports",
"/udp": "invalid range format for --expose: /udp, error: empty string specified for ports",
"NaN/tcp": `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
"NaN-NaN/tcp": `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
"8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
"1234567890-8080/tcp": `invalid range format for --expose: 1234567890-8080/tcp, error: strconv.ParseUint: parsing "1234567890": value out of range`,
}
valids := map[string][]nat.Port{
"8080/tcp": {"8080/tcp"},
"8080/udp": {"8080/udp"},
"8080/ncp": {"8080/ncp"},
"8080-8080/udp": {"8080/udp"},
"8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"},
}
for expose, expectedError := range invalids {
if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError {
t.Fatalf("Expected error '%v' with '--expose=%v', got '%v'", expectedError, expose, err)
}
for expose, expectedError := range tests {
t.Run(expose, func(t *testing.T) {
_, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"})
assert.Error(t, err, expectedError)
})
}
for expose, exposedPorts := range valids {
config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"})
if err != nil {
t.Fatal(err)
}
})
t.Run("valid", func(t *testing.T) {
tests := map[string][]networktypes.Port{
"8080/tcp": {networktypes.MustParsePort("8080/tcp")},
"8080/udp": {networktypes.MustParsePort("8080/udp")},
"8080/ncp": {networktypes.MustParsePort("8080/ncp")},
"8080-8080/udp": {networktypes.MustParsePort("8080/udp")},
"8080-8082/tcp": {networktypes.MustParsePort("8080/tcp"), networktypes.MustParsePort("8081/tcp"), networktypes.MustParsePort("8082/tcp")},
if len(config.ExposedPorts) != len(exposedPorts) {
t.Fatalf("Expected %v exposed port, got %v", len(exposedPorts), len(config.ExposedPorts))
}
for expose, exposedPorts := range tests {
t.Run(expose, func(t *testing.T) {
config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"})
assert.NilError(t, err)
for _, port := range exposedPorts {
_, ok := config.ExposedPorts[port]
assert.Check(t, ok, "missing port %q in exposed ports: %#+v", port, config.ExposedPorts[port])
}
})
for _, port := range exposedPorts {
if _, ok := config.ExposedPorts[port]; !ok {
t.Fatalf("Expected %v, got %v", exposedPorts, config.ExposedPorts)
}
}
})
t.Run("merge with published", func(t *testing.T) {
// Merge with actual published port
config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"})
assert.NilError(t, err)
assert.Check(t, is.Len(config.ExposedPorts, 2))
ports := []networktypes.Port{networktypes.MustParsePort("80/tcp"), networktypes.MustParsePort("81/tcp")}
for _, port := range ports {
_, ok := config.ExposedPorts[port]
assert.Check(t, ok, "missing port %q in exposed ports: %#+v", port, config.ExposedPorts[port])
}
// Merge with actual published port
config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"})
if err != nil {
t.Fatal(err)
}
if len(config.ExposedPorts) != 2 {
t.Fatalf("Expected 2 exposed ports, got %v", config.ExposedPorts)
}
ports := []nat.Port{"80/tcp", "81/tcp"}
for _, port := range ports {
if _, ok := config.ExposedPorts[port]; !ok {
t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts)
}
})
}
}
func TestParseDevice(t *testing.T) {
@ -581,6 +576,7 @@ func TestParseNetworkConfig(t *testing.T) {
name string
flags []string
expected map[string]*networktypes.EndpointSettings
expectedCfg container.Config
expectedHostCfg container.HostConfig
expectedErr string
}{
@ -612,9 +608,9 @@ func TestParseNetworkConfig(t *testing.T) {
expected: map[string]*networktypes.EndpointSettings{
"net1": {
IPAMConfig: &networktypes.EndpointIPAMConfig{
IPv4Address: netip.MustParseAddr("172.20.88.22"),
IPv6Address: netip.MustParseAddr("2001:db8::8822"),
LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.2.2"), netip.MustParseAddr("fe80::169:254:2:2")},
IPv4Address: "172.20.88.22",
IPv6Address: "2001:db8::8822",
LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"},
},
Links: []string{"foo:bar", "bar:baz"},
Aliases: []string{"web1", "web2"},
@ -642,9 +638,9 @@ func TestParseNetworkConfig(t *testing.T) {
"net1": {
DriverOpts: map[string]string{"field1": "value1"},
IPAMConfig: &networktypes.EndpointIPAMConfig{
IPv4Address: netip.MustParseAddr("172.20.88.22"),
IPv6Address: netip.MustParseAddr("2001:db8::8822"),
LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.2.2"), netip.MustParseAddr("fe80::169:254:2:2")},
IPv4Address: "172.20.88.22",
IPv6Address: "2001:db8::8822",
LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"},
},
Links: []string{"foo:bar", "bar:baz"},
Aliases: []string{"web1", "web2"},
@ -653,15 +649,15 @@ func TestParseNetworkConfig(t *testing.T) {
"net3": {
DriverOpts: map[string]string{"field3": "value3"},
IPAMConfig: &networktypes.EndpointIPAMConfig{
IPv4Address: netip.MustParseAddr("172.20.88.22"),
IPv6Address: netip.MustParseAddr("2001:db8::8822"),
IPv4Address: "172.20.88.22",
IPv6Address: "2001:db8::8822",
},
Aliases: []string{"web3"},
},
"net4": {
MacAddress: mustParseMAC("02:32:1c:23:00:04"),
MacAddress: "02:32:1c:23:00:04",
IPAMConfig: &networktypes.EndpointIPAMConfig{
LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.169.254")},
LinkLocalIPs: []string{"169.254.169.254"},
},
},
},
@ -677,13 +673,14 @@ func TestParseNetworkConfig(t *testing.T) {
"field2": "value2",
},
IPAMConfig: &networktypes.EndpointIPAMConfig{
IPv4Address: netip.MustParseAddr("172.20.88.22"),
IPv6Address: netip.MustParseAddr("2001:db8::8822"),
IPv4Address: "172.20.88.22",
IPv6Address: "2001:db8::8822",
},
Aliases: []string{"web1", "web2"},
MacAddress: mustParseMAC("02:32:1c:23:00:04"),
MacAddress: "02:32:1c:23:00:04",
},
},
expectedCfg: container.Config{MacAddress: "02:32:1c:23:00:04"},
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
},
{
@ -698,9 +695,10 @@ func TestParseNetworkConfig(t *testing.T) {
expected: map[string]*networktypes.EndpointSettings{
"net1": {
Aliases: []string{"foobar"},
MacAddress: mustParseMAC("52:0f:f3:dc:50:10"),
MacAddress: "52:0f:f3:dc:50:10",
},
},
expectedCfg: container.Config{MacAddress: "52:0f:f3:dc:50:10"},
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
},
{
@ -747,7 +745,7 @@ func TestParseNetworkConfig(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, hConfig, nwConfig, err := parseRun(tc.flags)
config, hConfig, nwConfig, err := parseRun(tc.flags)
if tc.expectedErr != "" {
assert.Error(t, err, tc.expectedErr)
@ -755,8 +753,9 @@ func TestParseNetworkConfig(t *testing.T) {
}
assert.NilError(t, err)
assert.DeepEqual(t, config.MacAddress, tc.expectedCfg.MacAddress) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
assert.DeepEqual(t, hConfig.NetworkMode, tc.expectedHostCfg.NetworkMode)
assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected, cmpopts.EquateComparable(netip.Addr{}))
assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected)
})
}
}
@ -1018,7 +1017,7 @@ func TestParseLabelfileVariables(t *testing.T) {
func TestParseEntryPoint(t *testing.T) {
config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"})
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(config.Entrypoint, []string{"anything"}))
assert.Check(t, is.DeepEqual([]string(config.Entrypoint), []string{"anything"}))
}
func TestValidateDevice(t *testing.T) {

View File

@ -8,8 +8,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"github.com/spf13/cobra"
)
@ -17,7 +16,13 @@ type pauseOptions struct {
containers []string
}
// newPauseCommand creates a new cobra.Command for "docker container pause"
// NewPauseCommand creates a new cobra.Command for `docker pause`
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewPauseCommand(dockerCLI command.Cli) *cobra.Command {
return newPauseCommand(dockerCLI)
}
func newPauseCommand(dockerCLI command.Cli) *cobra.Command {
var opts pauseOptions
@ -35,16 +40,12 @@ func newPauseCommand(dockerCLI command.Cli) *cobra.Command {
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool {
return ctr.State != container.StatePaused
}),
DisableFlagsInUseLine: true,
}
}
func runPause(ctx context.Context, dockerCLI command.Cli, opts *pauseOptions) error {
apiClient := dockerCLI.Client()
errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error {
_, err := apiClient.ContainerPause(ctx, container, client.ContainerPauseOptions{})
return err
})
errChan := parallelOperation(ctx, opts.containers, apiClient.ContainerPause)
var errs []error
for _, ctr := range opts.containers {

View File

@ -8,13 +8,18 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestRunPause(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli := test.NewFakeCli(
&fakeClient{
containerPauseFunc: func(ctx context.Context, container string) error {
return nil
},
},
)
cmd := newPauseCommand(cli)
cmd.SetOut(io.Discard)
@ -36,8 +41,8 @@ func TestRunPause(t *testing.T) {
func TestRunPauseClientError(t *testing.T) {
cli := test.NewFakeCli(
&fakeClient{
containerPauseFunc: func(ctx context.Context, container string, options client.ContainerPauseOptions) (client.ContainerPauseResult, error) {
return client.ContainerPauseResult{}, fmt.Errorf("client error for container %s", container)
containerPauseFunc: func(ctx context.Context, container string) error {
return fmt.Errorf("client error for container %s", container)
},
},
)

View File

@ -5,14 +5,15 @@ import (
"fmt"
"net"
"sort"
"strconv"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/go-connections/nat"
"github.com/fvbommel/sortorder"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -22,8 +23,14 @@ type portOptions struct {
port string
}
// newPortCommand creates a new cobra.Command for "docker container port".
func newPortCommand(dockerCLI command.Cli) *cobra.Command {
// NewPortCommand creates a new cobra.Command for `docker port`
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewPortCommand(dockerCli command.Cli) *cobra.Command {
return newPortCommand(dockerCli)
}
func newPortCommand(dockerCli command.Cli) *cobra.Command {
var opts portOptions
cmd := &cobra.Command{
@ -35,13 +42,12 @@ func newPortCommand(dockerCLI command.Cli) *cobra.Command {
if len(args) > 1 {
opts.port = args[1]
}
return runPort(cmd.Context(), dockerCLI, &opts)
return runPort(cmd.Context(), dockerCli, &opts)
},
Annotations: map[string]string{
"aliases": "docker container port, docker port",
},
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
DisableFlagsInUseLine: true,
ValidArgsFunction: completion.ContainerNames(dockerCli, false),
}
return cmd
}
@ -53,28 +59,31 @@ func newPortCommand(dockerCLI command.Cli) *cobra.Command {
// proto is specified. We should consider changing this to "any" protocol
// for the given private port.
func runPort(ctx context.Context, dockerCli command.Cli, opts *portOptions) error {
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container, client.ContainerInspectOptions{})
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container)
if err != nil {
return err
}
var out []string
if opts.port != "" {
port, err := network.ParsePort(opts.port)
if err != nil {
return err
port, proto, _ := strings.Cut(opts.port, "/")
if proto == "" {
proto = "tcp"
}
frontends, exists := c.Container.NetworkSettings.Ports[port]
if _, err = strconv.ParseUint(port, 10, 16); err != nil {
return errors.Wrapf(err, "Error: invalid port (%s)", port)
}
frontends, exists := c.NetworkSettings.Ports[nat.Port(port+"/"+proto)]
if !exists || len(frontends) == 0 {
return fmt.Errorf("no public port '%s' published for %s", opts.port, opts.container)
return errors.Errorf("Error: No public port '%s' published for %s", opts.port, opts.container)
}
for _, frontend := range frontends {
out = append(out, net.JoinHostPort(frontend.HostIP.String(), frontend.HostPort))
out = append(out, net.JoinHostPort(frontend.HostIP, frontend.HostPort))
}
} else {
for from, frontends := range c.Container.NetworkSettings.Ports {
for from, frontends := range c.NetworkSettings.Ports {
for _, frontend := range frontends {
out = append(out, fmt.Sprintf("%s -> %s", from, net.JoinHostPort(frontend.HostIP.String(), frontend.HostPort)))
out = append(out, fmt.Sprintf("%s -> %s", from, net.JoinHostPort(frontend.HostIP, frontend.HostPort)))
}
}
}

View File

@ -2,13 +2,11 @@ package container
import (
"io"
"net/netip"
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"github.com/docker/go-connections/nat"
"gotest.tools/v3/assert"
"gotest.tools/v3/golden"
)
@ -16,56 +14,56 @@ import (
func TestNewPortCommandOutput(t *testing.T) {
testCases := []struct {
name string
ips []netip.Addr
ips []string
port string
}{
{
name: "container-port-ipv4",
ips: []netip.Addr{netip.MustParseAddr("0.0.0.0")},
ips: []string{"0.0.0.0"},
port: "80",
},
{
name: "container-port-ipv6",
ips: []netip.Addr{netip.MustParseAddr("::")},
ips: []string{"::"},
port: "80",
},
{
name: "container-port-ipv6-and-ipv4",
ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")},
ips: []string{"::", "0.0.0.0"},
port: "80",
},
{
name: "container-port-ipv6-and-ipv4-443-udp",
ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")},
ips: []string{"::", "0.0.0.0"},
port: "443/udp",
},
{
name: "container-port-all-ports",
ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")},
ips: []string{"::", "0.0.0.0"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
inspectFunc: func(string) (client.ContainerInspectResult, error) {
inspectFunc: func(string) (container.InspectResponse, error) {
ci := container.InspectResponse{NetworkSettings: &container.NetworkSettings{}}
ci.NetworkSettings.Ports = network.PortMap{
network.MustParsePort("80/tcp"): make([]network.PortBinding, len(tc.ips)),
network.MustParsePort("443/tcp"): make([]network.PortBinding, len(tc.ips)),
network.MustParsePort("443/udp"): make([]network.PortBinding, len(tc.ips)),
ci.NetworkSettings.Ports = nat.PortMap{
"80/tcp": make([]nat.PortBinding, len(tc.ips)),
"443/tcp": make([]nat.PortBinding, len(tc.ips)),
"443/udp": make([]nat.PortBinding, len(tc.ips)),
}
for i, ip := range tc.ips {
ci.NetworkSettings.Ports[network.MustParsePort("80/tcp")][i] = network.PortBinding{
ci.NetworkSettings.Ports["80/tcp"][i] = nat.PortBinding{
HostIP: ip, HostPort: "3456",
}
ci.NetworkSettings.Ports[network.MustParsePort("443/tcp")][i] = network.PortBinding{
ci.NetworkSettings.Ports["443/tcp"][i] = nat.PortBinding{
HostIP: ip, HostPort: "4567",
}
ci.NetworkSettings.Ports[network.MustParsePort("443/udp")][i] = network.PortBinding{
ci.NetworkSettings.Ports["443/udp"][i] = nat.PortBinding{
HostIP: ip, HostPort: "5678",
}
}
return client.ContainerInspectResult{Container: ci}, nil
return ci, nil
},
})
cmd := newPortCommand(cli)

View File

@ -2,33 +2,29 @@ package container
import (
"context"
"errors"
"fmt"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/system/pruner"
"github.com/docker/cli/internal/prompt"
"github.com/docker/cli/opts"
"github.com/docker/go-units"
"github.com/moby/moby/client"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
func init() {
// Register the prune command to run as part of "docker system prune"
if err := pruner.Register(pruner.TypeContainer, pruneFn); err != nil {
panic(err)
}
}
type pruneOptions struct {
force bool
filter opts.FilterOpt
}
// newPruneCommand returns a new cobra prune command for containers.
// NewPruneCommand returns a new cobra prune command for containers
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewPruneCommand(dockerCLI command.Cli) *cobra.Command {
return newPruneCommand(dockerCLI)
}
func newPruneCommand(dockerCLI command.Cli) *cobra.Command {
options := pruneOptions{filter: opts.NewFilterOpt()}
@ -47,9 +43,8 @@ func newPruneCommand(dockerCLI command.Cli) *cobra.Command {
fmt.Fprintln(dockerCLI.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
return nil
},
Annotations: map[string]string{"version": "1.25"},
ValidArgsFunction: cobra.NoFileCompletions,
DisableFlagsInUseLine: true,
Annotations: map[string]string{"version": "1.25"},
ValidArgsFunction: cobra.NoFileCompletions,
}
flags := cmd.Flags()
@ -62,7 +57,7 @@ func newPruneCommand(dockerCLI command.Cli) *cobra.Command {
const warning = `WARNING! This will remove all stopped containers.
Are you sure you want to continue?`
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, _ error) {
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
if !options.force {
@ -75,39 +70,28 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
}
}
res, err := dockerCli.Client().ContainerPrune(ctx, client.ContainerPruneOptions{
Filters: pruneFilters,
})
report, err := dockerCli.Client().ContainersPrune(ctx, pruneFilters)
if err != nil {
return 0, "", err
}
var out strings.Builder
if len(res.Report.ContainersDeleted) > 0 {
out.WriteString("Deleted Containers:\n")
for _, id := range res.Report.ContainersDeleted {
out.WriteString(id + "\n")
if len(report.ContainersDeleted) > 0 {
output = "Deleted Containers:\n"
for _, id := range report.ContainersDeleted {
output += id + "\n"
}
spaceReclaimed = res.Report.SpaceReclaimed
spaceReclaimed = report.SpaceReclaimed
}
return spaceReclaimed, out.String(), nil
return spaceReclaimed, output, nil
}
type cancelledErr struct{ error }
func (cancelledErr) Cancelled() {}
// pruneFn calls the Container Prune API for use in "docker system prune",
// and returns the amount of space reclaimed and a detailed output string.
func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) {
if !options.Confirmed {
// Dry-run: perform validation and produce confirmation before pruning.
confirmMsg := "all stopped containers"
return 0, confirmMsg, cancelledErr{errors.New("containers prune has been cancelled")}
}
return runPrune(ctx, dockerCLI, pruneOptions{
force: true,
filter: options.Filter,
})
// RunPrune calls the Container Prune API
// This returns the amount of space reclaimed and a detailed output string
func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.FilterOpt) (uint64, string, error) {
return runPrune(ctx, dockerCli, pruneOptions{force: true, filter: filter})
}

View File

@ -7,7 +7,8 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
)
func TestContainerPrunePromptTermination(t *testing.T) {
@ -15,8 +16,8 @@ func TestContainerPrunePromptTermination(t *testing.T) {
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
containerPruneFunc: func(ctx context.Context, opts client.ContainerPruneOptions) (client.ContainerPruneResult, error) {
return client.ContainerPruneResult{}, errors.New("fakeClient containerPruneFunc should not be called")
containerPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
return container.PruneReport{}, errors.New("fakeClient containerPruneFunc should not be called")
},
})
cmd := newPruneCommand(cli)

View File

@ -1,36 +1,60 @@
package container
import (
"context"
"fmt"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/moby/moby/client"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
// newRenameCommand creates a new cobra.Command for "docker container rename".
type renameOptions struct {
oldName string
newName string
}
// NewRenameCommand creates a new cobra.Command for `docker rename`
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewRenameCommand(dockerCLI command.Cli) *cobra.Command {
return newRenameCommand(dockerCLI)
}
func newRenameCommand(dockerCLI command.Cli) *cobra.Command {
var opts renameOptions
cmd := &cobra.Command{
Use: "rename CONTAINER NEW_NAME",
Short: "Rename a container",
Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
oldName, newName := args[0], args[1]
_, err := dockerCLI.Client().ContainerRename(cmd.Context(), oldName, client.ContainerRenameOptions{
NewName: newName,
})
if err != nil {
return fmt.Errorf("failed to rename container: %w", err)
}
return nil
opts.oldName = args[0]
opts.newName = args[1]
return runRename(cmd.Context(), dockerCLI, &opts)
},
Annotations: map[string]string{
"aliases": "docker container rename, docker rename",
},
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
DisableFlagsInUseLine: true,
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
}
return cmd
}
func runRename(ctx context.Context, dockerCli command.Cli, opts *renameOptions) error {
oldName := strings.TrimSpace(opts.oldName)
newName := strings.TrimSpace(opts.newName)
if oldName == "" || newName == "" {
return errors.New("Error: Neither old nor new names may be empty")
}
if err := dockerCli.Client().ContainerRename(ctx, oldName, newName); err != nil {
fmt.Fprintln(dockerCli.Err(), err)
return errors.Errorf("Error: failed to rename container named %s", oldName)
}
return nil
}

View File

@ -0,0 +1,77 @@
package container
import (
"context"
"errors"
"io"
"testing"
"github.com/docker/cli/internal/test"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestRunRename(t *testing.T) {
testcases := []struct {
doc, oldName, newName, expectedErr string
}{
{
doc: "success",
oldName: "oldName",
newName: "newName",
expectedErr: "",
},
{
doc: "empty old name",
oldName: "",
newName: "newName",
expectedErr: "Error: Neither old nor new names may be empty",
},
{
doc: "empty new name",
oldName: "oldName",
newName: "",
expectedErr: "Error: Neither old nor new names may be empty",
},
}
for _, tc := range testcases {
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerRenameFunc: func(ctx context.Context, oldName, newName string) error {
return nil
},
})
cmd := newRenameCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{tc.oldName, tc.newName})
err := cmd.Execute()
if tc.expectedErr != "" {
assert.ErrorContains(t, err, tc.expectedErr)
} else {
assert.NilError(t, err)
}
})
}
}
func TestRunRenameClientError(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerRenameFunc: func(ctx context.Context, oldName, newName string) error {
return errors.New("client error")
},
})
cmd := newRenameCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{"oldName", "newName"})
err := cmd.Execute()
assert.Check(t, is.Error(err, "Error: failed to rename container named oldName"))
}

View File

@ -8,7 +8,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"github.com/spf13/cobra"
)
@ -20,7 +20,13 @@ type restartOptions struct {
containers []string
}
// newRestartCommand creates a new cobra.Command for "docker container restart".
// NewRestartCommand creates a new cobra.Command for `docker restart`
//
// Deprecated: Do not import commands directly. They will be removed in a future release.
func NewRestartCommand(dockerCLI command.Cli) *cobra.Command {
return newRestartCommand(dockerCLI)
}
func newRestartCommand(dockerCLI command.Cli) *cobra.Command {
var opts restartOptions
@ -39,8 +45,7 @@ func newRestartCommand(dockerCLI command.Cli) *cobra.Command {
Annotations: map[string]string{
"aliases": "docker container restart, docker restart",
},
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
DisableFlagsInUseLine: true,
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
}
flags := cmd.Flags()
@ -66,7 +71,7 @@ func runRestart(ctx context.Context, dockerCLI command.Cli, opts *restartOptions
var errs []error
// TODO(thaJeztah): consider using parallelOperation for restart, similar to "stop" and "remove"
for _, name := range opts.containers {
_, err := apiClient.ContainerRestart(ctx, name, client.ContainerRestartOptions{
err := apiClient.ContainerRestart(ctx, name, container.StopOptions{
Signal: opts.signal,
Timeout: timeout,
})

View File

@ -9,7 +9,7 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/client"
"github.com/docker/docker/api/types/container"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
@ -19,7 +19,7 @@ func TestRestart(t *testing.T) {
name string
args []string
restarted []string
expectedOpts client.ContainerRestartOptions
expectedOpts container.StopOptions
expectedErr string
}{
{
@ -36,19 +36,19 @@ func TestRestart(t *testing.T) {
{
name: "with -t",
args: []string{"-t", "2", "container-1"},
expectedOpts: client.ContainerRestartOptions{Timeout: func(to int) *int { return &to }(2)},
expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)},
restarted: []string{"container-1"},
},
{
name: "with --timeout",
args: []string{"--timeout", "2", "container-1"},
expectedOpts: client.ContainerRestartOptions{Timeout: func(to int) *int { return &to }(2)},
expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)},
restarted: []string{"container-1"},
},
{
name: "with --time",
args: []string{"--time", "2", "container-1"},
expectedOpts: client.ContainerRestartOptions{Timeout: func(to int) *int { return &to }(2)},
expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)},
restarted: []string{"container-1"},
},
{
@ -62,17 +62,17 @@ func TestRestart(t *testing.T) {
mutex := new(sync.Mutex)
cli := test.NewFakeCli(&fakeClient{
containerRestartFunc: func(ctx context.Context, containerID string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error) {
containerRestartFunc: func(ctx context.Context, containerID string, options container.StopOptions) error {
assert.Check(t, is.DeepEqual(options, tc.expectedOpts))
if containerID == "nosuchcontainer" {
return client.ContainerRestartResult{}, notFound(errors.New("Error: no such container: " + containerID))
return notFound(errors.New("Error: no such container: " + containerID))
}
// TODO(thaJeztah): consider using parallelOperation for restart, similar to "stop" and "remove"
mutex.Lock()
restarted = append(restarted, containerID)
mutex.Unlock()
return client.ContainerRestartResult{}, nil
return nil
},
Version: "1.36",
})

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