Compare commits

..

117 Commits

Author SHA1 Message Date
2355349d8b Merge pull request #2119 from thaJeztah/19.03_backport_bump_yaml.v2_2.2.3
[19.03 backport] vendor: Bump gopkg.in/yaml.v2
2019-10-03 10:52:44 -07:00
73d513e56a Merge pull request #2120 from thaJeztah/19.03_backport_experimental_note
[19.03 backport] backport docs changes
2019-10-01 14:27:56 -07:00
9fd5604d80 Adding experimental feature info
Signed-off-by: Adrian Plata <adrian.plata@docker.com>
(cherry picked from commit 031bd2d530)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-10-01 19:11:06 +02:00
82ac89901f Clarifying docker config rules
Signed-off-by: Adrian Plata <adrian.plata@docker.com>
(cherry picked from commit 131eefc54f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-10-01 19:11:01 +02:00
29e3a70b04 vendor: Bump gopkg.in/yaml.v2
Signed-off-by: Christopher Crone <christopher.crone@docker.com>
(cherry picked from commit 91cf8b04c9)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-10-01 14:54:49 +02:00
3fb239756d Merge pull request #2113 from thaJeztah/19.03_backport_bump_golang_1.12.10
[19.03 backport] bump golang 1.12.10 (CVE-2019-16276)
2019-09-28 01:53:31 +02:00
ff72c27a7a bump golang 1.12.10 (CVE-2019-16276)
full diff: https://github.com/golang/go/compare/go1.12.9...go1.12.10

```
Hi gophers,

We have just released Go 1.13.1 and Go 1.12.10 to address a recently reported security issue. We recommend that all affected users update to one of these releases (if you're not sure which, choose Go 1.13.1).

net/http (through net/textproto) used to accept and normalize invalid HTTP/1.1 headers with a space before the colon, in violation of RFC 7230. If a Go server is used behind an uncommon reverse proxy that accepts and forwards but doesn't normalize such invalid headers, the reverse proxy and the server can interpret the headers differently. This can lead to filter bypasses or request smuggling, the latter if requests from separate clients are multiplexed onto the same upstream connection by the proxy. Such invalid headers are now rejected by Go servers, and passed without normalization to Go client applications.

The issue is CVE-2019-16276 and Go issue golang.org/issue/34540.

Thanks to Andrew Stucki, Adam Scarr (99designs.com), and Jan Masarik (masarik.sh) for discovering and reporting this issue.

Downloads are available at https://golang.org/dl for all supported platforms.

Alla prossima,
Filippo on behalf of the Go team
```

From the patch: 6e6f4aaf70

```
net/textproto: don't normalize headers with spaces before the colon

RFC 7230 is clear about headers with a space before the colon, like

X-Answer : 42

being invalid, but we've been accepting and normalizing them for compatibility
purposes since CL 5690059 in 2012.

On the client side, this is harmless and indeed most browsers behave the same
to this day. On the server side, this becomes a security issue when the
behavior doesn't match that of a reverse proxy sitting in front of the server.

For example, if a WAF accepts them without normalizing them, it might be
possible to bypass its filters, because the Go server would interpret the
header differently. Worse, if the reverse proxy coalesces requests onto a
single HTTP/1.1 connection to a Go server, the understanding of the request
boundaries can get out of sync between them, allowing an attacker to tack an
arbitrary method and path onto a request by other clients, including
authentication headers unknown to the attacker.

This was recently presented at multiple security conferences:
https://portswigger.net/blog/http-desync-attacks-request-smuggling-reborn

net/http servers already reject header keys with invalid characters.
Simply stop normalizing extra spaces in net/textproto, let it return them
unchanged like it does for other invalid headers, and let net/http enforce
RFC 7230, which is HTTP specific. This loses us normalization on the client
side, but there's no right answer on the client side anyway, and hiding the
issue sounds worse than letting the application decide.
```

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 8743e36a45)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-09-27 23:33:06 +02:00
ed54e85144 Merge pull request #2079 from thaJeztah/19.03_backport_bump_pkcs11
[19.03 backport] bump miekg/pkcs11 v1.0.2
2019-09-27 13:41:10 +02:00
0bc3d1fd2d bump miekg/pkcs11 v1.0.2
full diff: 6120d95c0e...v1.0.2

relevant changes:

- miekg/pkcs11#110 Fix issue freeing memory on GetOperationState when NOT CK_OK
- miekg/pkcs11#106 Move to go modules
- miekg/pkcs11#104 Expose login API for vendor specific login types

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 54428b1f37)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-09-27 11:51:03 +02:00
50bb8c70f3 Merge pull request #2102 from tiborvass/19.03-buildkit-vendor
[19.03] vendor: update buildkit to docker-19.03
2019-09-23 18:18:48 -07:00
25168137bd vendor: update buildkit to docker-19.03
Signed-off-by: Tibor Vass <tibor@docker.com>
2019-09-24 00:36:30 +00:00
3af5c3f8c6 Merge pull request #2099 from thaJeztah/19.03_backport_bump_vndr
[19.03 backport] bump LK4D4/vndr v0.0.3 and revendor
2019-09-23 16:59:17 -07:00
3143ba5700 bump LK4D4/vndr v0.0.3 and revendor
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 909b85460c)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-09-23 19:30:55 +02:00
a89484e338 Merge pull request #2090 from thaJeztah/19.03_backport_bump_go_1.12.9
[19.03 backport] Bump golang 1.12.9
2019-09-23 10:21:02 -07:00
2ac9213069 Bump golang 1.12.9
Signed-off-by: Jintao Zhang <zhangjintao9020@gmail.com>
(cherry picked from commit d9856049e5)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-09-16 16:35:02 +02:00
578ab52ece Merge pull request #2048 from thaJeztah/19.03_backport_ci_improvements
[19.03 backport] CI and testing improvements
2019-08-22 10:57:08 -07:00
c8e9c04d19 Merge pull request #2061 from thaJeztah/19.03_backport_issue39654
[19.03 backport] restore support for env variables to configure proxy
2019-08-22 19:55:05 +02:00
2fead2a50f restore support for env variables to configure proxy
regression introduced by b34f34
close #39654

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
(cherry picked from commit e25e077a20)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-08-22 19:11:41 +02:00
df1fe15cf6 Merge pull request #1985 from thaJeztah/19.03_backport_consistent_output_on_context_create
[19.03 backport] context: produce consistent output on `context create`.
2019-08-22 10:19:01 +02:00
be9adbd5c1 e2e: remove docker engine testing remnants
These changes were made as part of the `docker engine` feature
in commit fd2f1b3b66, but later
reverted in f250152bf4 and
b7ec4a42d9

These lines were forgotten to be removed, and should no longer
be needed.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit de01e72455)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-08-15 03:19:38 +02:00
2907276eca e2e: enable buildkit
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 893db86d6e)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-08-15 03:18:42 +02:00
59b02c04bf Circle-CI: use progress=plain
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit ae58e356ea)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-08-15 03:18:39 +02:00
6a3eb417d5 Circle-CI: enable buildkit
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9a6519db76)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-08-15 03:18:37 +02:00
c30ccb308d Update dockerignore
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 82e01807bc)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-08-15 03:18:34 +02:00
1572845a2f Update CircleCI Docker version to 18.09.3
18.03 has reached EOL; let's use a more current version in CI

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 8b19c1d73a)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-08-15 03:16:52 +02:00
caad34cf58 Circle-CI: fix indentation in circle.yml
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 53fc63a93f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-08-15 03:16:49 +02:00
bf683dfe52 Merge pull request #2044 from thaJeztah/19.03_backport_bump_golang_1.12.8
[19.03 backport] Bump golang 1.12.8 (CVE-2019-9512, CVE-2019-9514)
2019-08-14 11:55:59 -07:00
307befd7e2 Adjust tests for changes in Go 1.12.8 / 1.11.13
For now, just verifying that an error is returned, but not checking the
error message itself, because those are not under our control, and may
change with different Go versions.

```
=== Failed
=== FAIL: opts TestParseDockerDaemonHost (0.00s)
    hosts_test.go:87: tcp tcp:a.b.c.d address expected error "Invalid bind address format: tcp:a.b.c.d" return, got "parse tcp://tcp:a.b.c.d: invalid port \":a.b.c.d\" after host" and addr
    hosts_test.go:87: tcp tcp:a.b.c.d/path address expected error "Invalid bind address format: tcp:a.b.c.d/path" return, got "parse tcp://tcp:a.b.c.d/path: invalid port \":a.b.c.d\" after host" and addr

=== FAIL: opts TestParseTCP (0.00s)
    hosts_test.go:129: tcp tcp:a.b.c.d address expected error Invalid bind address format: tcp:a.b.c.d return, got parse tcp://tcp:a.b.c.d: invalid port ":a.b.c.d" after host and addr
    hosts_test.go:129: tcp tcp:a.b.c.d/path address expected error Invalid bind address format: tcp:a.b.c.d/path return, got parse tcp://tcp:a.b.c.d/path: invalid port ":a.b.c.d" after host and addr
```

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit de1523d221)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-08-14 10:20:43 +02:00
b58270ba69 Bump golang 1.12.8 (CVE-2019-9512, CVE-2019-9514)
go1.12.8 (released 2019/08/13) includes security fixes to the net/http and net/url packages.
See the Go 1.12.8 milestone on our issue tracker for details:

https://github.com/golang/go/issues?q=milestone%3AGo1.12.8

- net/http: Denial of Service vulnerabilities in the HTTP/2 implementation
  net/http and golang.org/x/net/http2 servers that accept direct connections from untrusted
  clients could be remotely made to allocate an unlimited amount of memory, until the program
  crashes. Servers will now close connections if the send queue accumulates too many control
  messages.
  The issues are CVE-2019-9512 and CVE-2019-9514, and Go issue golang.org/issue/33606.
  Thanks to Jonathan Looney from Netflix for discovering and reporting these issues.
  This is also fixed in version v0.0.0-20190813141303-74dc4d7220e7 of golang.org/x/net/http2.
  net/url: parsing validation issue
- url.Parse would accept URLs with malformed hosts, such that the Host field could have arbitrary
  suffixes that would appear in neither Hostname() nor Port(), allowing authorization bypasses
  in certain applications. Note that URLs with invalid, not numeric ports will now return an error
  from url.Parse.
  The issue is CVE-2019-14809 and Go issue golang.org/issue/29098.
  Thanks to Julian Hector and Nikolai Krein from Cure53, and Adi Cohen (adico.me) for discovering
  and reporting this issue.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit bbd179f25b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-08-14 03:03:11 +02:00
0ecfcb5997 Dockerfile: use GO_VERSION build-arg for overriding Go version
This allows overriding the version of Go without making modifications in the
source code, which can be useful to test against multiple versions.

For example:

    make GO_VERSION=1.13beta1 -f docker.Makefile binary

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 0d3022c6d2)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-08-14 03:03:04 +02:00
0ea69840c6 Merge pull request #1970 from thaJeztah/19.03_backport_skip_windows_permissions_check
[19.03 backport] Windows: skip permissions check on key
2019-08-09 20:17:09 +02:00
208de55a17 Merge pull request #1983 from thaJeztah/19.03_backport_bump_credential_helpers
[19.03 backport] bump docker-credential-helpers v0.6.3
2019-08-07 19:02:50 -07:00
1a8077b814 Merge pull request #1998 from thaJeztah/19.03_backport_bump_golang_1.12.7
[19.03 backport] Bump golang 1.12.7
2019-08-07 17:48:16 -07:00
fa0e2597e6 Merge pull request #2022 from thaJeztah/19.03_backport_fix_e2e
[19.03 backport] Disable TLS for e2e docker-in-docker daemon
2019-08-07 17:29:48 -07:00
f357def036 Disable TLS for e2e docker-in-docker daemon
The docker-in-docker image now enables TLS by default (added in
docker-library/docker#166), which complicates testing in our
environment, and isn't needed for the tests we're running.

This patch sets the `DOCKER_TLS_CERTDIR` to an empty value to
disable TLS.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit b1a3c1aad1)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-07-29 17:11:28 -07:00
792ce891be e2e: use stable-dind image for testing
The edge channel is deprecated and no longer updated

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 08fd6dd63c)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-07-29 17:11:25 -07:00
d473c60571 Merge pull request #1995 from thaJeztah/19.03_backport_cross_platform_bind
[19.03 backport] Detect Windows absolute paths on non-Windows CLI
2019-07-26 13:11:18 -07:00
b020a36d10 Merge pull request #2001 from thaJeztah/19.03_backport_docs
[19.03 backport] assorted docs and completion script fixes
2019-07-23 14:16:08 -07:00
d2e8ff9e20 bump docker-credential-helpers v0.6.3
full diff: https://github.com/docker/docker-credential-helpers/compare/v0.6.2...v0.6.3

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 64f0ae4252)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-07-17 17:02:21 +02:00
10a899b6bd fix: docker login autocomplete for zsh
Changed `--user` to `--username`

Signed-off-by: Rohan Verma <hello@rohanverma.net>
(cherry picked from commit 1dc756e8df)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-07-12 16:14:52 +02:00
41718b98f6 adding nvidia gpu access info
Signed-off-by: Adrian Plata <adrian.plata@docker.com>

Removing prerequisites section.
Signed-off-by: Adrian Plata <adrian.plata@docker.com>

Removing prerequisites section.
Signed-off-by: Adrian Plata <adrian.plata@docker.com>

adding nvidia gpu access info
Signed-off-by: Adrian Plata <adrian.plata@docker.com>

Refining information.

Removing prerequisites section.

adding nvidia gpu access info
Signed-off-by: Adrian Plata <adrian.plata@docker.com>

Refining information.

Removing prerequisites section.

adding nvidia gpu access info
Signed-off-by: Adrian Plata <adrian.plata@docker.com>

Refining information.

Removing prerequisites section.

(cherry picked from commit f7b75eeb9b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-07-12 16:14:04 +02:00
caf21526a0 docs: add info for events backlog and scope
1. Adds `docker events` description info on the two scope types of events.
2. Adds `docker events` note in two places about backlog limit of event log.

Further info and background info in Issue 727

Signed-off-by: Bret Fisher <bret@bretfisher.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 988b9a0d96)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-07-12 16:13:22 +02:00
5b38d82aa0 Merge pull request #1972 from thaJeztah/19.03_backport_bump_compose_on_kube
[19.03 backport] bump compose-on-kubernetes v0.4.23
2019-07-11 10:57:10 -07:00
e303dfb6fd Merge pull request #1979 from thaJeztah/19.03_backport_fix_rollback_config_interpolation
[WIP][19.03 backport] Fix Rollback config type interpolation
2019-07-11 10:56:15 -07:00
94b98bfa21 Bump golang 1.12.7
go1.12.7 (released 2019/07/08) includes fixes to cgo, the compiler, and the
linker. See the Go 1.12.7 milestone on our issue tracker for details:
https://github.com/golang/go/issues?q=milestone%3AGo1.12.7

full diff: https://github.com/golang/go/compare/go1.12.6...go1.12.7

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit b06f9e9595)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-07-11 14:10:40 +02:00
87e400e44e Detect Windows absolute paths on non-Windows CLI
When deploying a stack using a relative path as bind-mount
source in the compose file, the CLI converts the relative
path to an absolute path, relative to the location of the
docker-compose file.

This causes a problem when deploying a stack that uses
an absolute Windows path, because a non-Windows client will
fail to detect that the path (e.g. `C:\somedir`) is an absolute
path (and not a relative directory named `C:\`).

The existing code did already take Windows clients deploying
a Linux stack into account (by checking if the path had a leading
slash). This patch adds the reverse, and adds detection for Windows
absolute paths on non-Windows clients.

The code used to detect Windows absolute paths is copied from the
Golang filepath package;
1d0e94b1e1/src/path/filepath/path_windows.go (L12-L65)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit d6dd08d568)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-07-10 23:40:22 +02:00
8cb2456248 context: produce consistent output on context create.
Refactor `RunCreate` slightly so that all three paths always produce the same
output, namely the name of the new context of `stdout` (for scripting) and the
success log message on `stderr`.

Validate by extending the existing unit tests to always check the output is as
expected.

Signed-off-by: Ian Campbell <ijc@docker.com>
(cherry picked from commit ff44305c47)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-07-04 21:37:25 +02:00
11b15544c5 bump docker-credential-helpers v0.6.2
full diff: 5241b46610...8a9f93a99f

includes:

- docker/docker-credential-helpers#29 C.free(unsafe.Pointer(err)) -> C.g_error_free(err)
- docker/docker-credential-helpers#124 pass: changed the way for checking if password-store is initalized
  - addresses docker/docker-credential-helpers#133 docker-credential-pass commits about 10 times every time I run a docker command
- docker/docker-credential-helpers#143 Fix docker-credential-osxkeychain list behaviour in case of missing entry in keychain
- docker/docker-credential-helpers#139 make docker-credential-wincred work like docker-credential-osxkeychain

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit f6a4c76fbb)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-07-04 21:12:56 +02:00
344adac7a6 Rollback config type interpolation on fields "parallelism" and "max_failure_ratio" were missing, as it uses the same type as update_config.
Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
(cherry picked from commit efdf36fa81)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-07-03 19:20:41 +02:00
2027d17a9d Merge pull request #1976 from thaJeztah/19.03_backport_deprecate_aufs
[19.03 backport] Deprecate AuFS storage driver
2019-07-02 13:44:40 -07:00
ff881608fb Deprecate AuFS storage driver
The `aufs` storage driver is deprecated in favor of `overlay2`, and will
be removed in a future release. Users of the `aufs` storage driver are
recommended to migrate to a different storage driver, such as `overlay2`, which
is now the default storage driver.

The `aufs` storage driver facilitates running Docker on distros that have no
support for OverlayFS, such as Ubuntu 14.04 LTS, which originally shipped with
a 3.14 kernel.

Now that Ubuntu 14.04 is no longer a supported distro for Docker, and `overlay2`
is available to all supported distros (as they are either on kernel 4.x, or have
support for multiple lowerdirs backported), there is no reason to continue
maintenance of the `aufs` storage driver.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit c8e9233b93)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-07-02 17:36:19 +02:00
8947ee2709 bump compose-on-kubernetes v0.4.23
no local changes

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 1877ed6aa3)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-06-26 13:10:51 +02:00
2f1931f9eb Merge pull request #1967 from thaJeztah/19.03_backport_fix_advanced_options_for_backward_compat
[19.03 backport] Fix advanced options for backward compatibility
2019-06-25 12:17:58 -07:00
8164090257 Merge pull request #1960 from thaJeztah/19.03_backport_deprecate_schema1
[19.03 backport] deprecate registry v2 schema 1
2019-06-25 15:56:41 +02:00
e803e487c3 Windows: skip permissions check on key
This code was attempting to check Linux file permissions
to determine if the key was accessible by other users, which
doesn't work, and therefore prevented users on Windows
to load keys.

Skipping this check on Windows (correspinding tests
were already skipped).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 15d361fd77)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-06-25 12:54:33 +02:00
5b636878fc deprecate registry v2 schema 1
Co-Authored-By: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 8b4e52f0bf)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-06-24 23:20:42 +02:00
3bc3f0390e Fix advanced options for backward compatibility
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)

This fixes an issue on older versions of legacy Swarm, which did not support
`NetworkingConfig.EndpointConfig`.

This was introduced in 5bc09639cc (#1767)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 4d7e6bf629)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-06-24 23:18:34 +02:00
296e10c0c5 Bump golang 1.12.6
Signed-off-by: Jintao Zhang <zhangjintao9020@gmail.com>
(cherry picked from commit 459099e175)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-06-13 15:52:38 +02:00
a63faebcf1 Merge pull request #1927 from cpuguy83/backport_goarm
[19.03] Backport #1857: Support GOARM and windows .exe in binary target
2019-06-06 21:10:27 +02:00
49236a4391 Merge pull request #1929 from thaJeztah/19.03_backport_fix_empty_context_import
[19.03 backport] Fix detection of invalid context files when importing
2019-06-06 21:09:29 +02:00
17b3250f0f Fix detection of invalid context files when importing
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 5f93509668)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-06-06 15:02:52 +02:00
5d246f4998 Support GOARM and windows .exe in binary target
This just makes it easier to build a targeted binary for the
goos/goach/goarm version.

This of course will not work for all cases but is nice to get things
going.
Specifically cross-compiling pkcs for yubikey support requires some
extra work whichis not tackled here.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
(cherry picked from commit 15130e3043)
Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2019-06-05 14:13:14 -07:00
f913afa98c Merge pull request #1912 from thaJeztah/19.03_backport_import_zip
[19.03 backport] Introduce .zip import for docker context
2019-05-29 23:53:59 +03:00
90f256aeab Introduce .zip import for docker context
Adds capabilities to import a .zip file with importZip.
Detects the content type of source by checking bytes & DetectContentType.
Adds LimitedReader reader, a fork of io.LimitedReader,
was needed for better error messaging instead of just getting back EOF.
We are using limited reader to avoid very big files causing memory issues.
Adds a new file size limit for context imports,
this limit is used for the main file for .zip & .tar and individual compressed
files for .zip.
Added TestImportZip that will check the import content type
Then will assert no err on Importing .zip file

Signed-off-by: Goksu Toprak <goksu.toprak@docker.com>
(cherry picked from commit 291e86289b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-29 23:09:20 +03:00
ee10970b05 Merge pull request #1911 from thaJeztah/19.03_bump_engine_19.03_3
[19.03 backport] bump docker/docker to tip of 19.03 branch
2019-05-27 23:00:57 +03:00
35c929ed5e bump docker/docker to tip of 19.03 branch
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-27 22:18:14 +03:00
60eb4ceaf7 Merge pull request #1886 from thaJeztah/19.03_bump_engine_19.03_2
[19.03] bump  bump docker/docker bff7e300e6bdb18c2417e23594bf26063a378dee (19.03)
2019-05-27 22:05:41 +03:00
1b15368c47 Merge pull request #1907 from silvin-lubecki/19.03_backport-reduce-vendoring-impact2
[19.03 backport] Dynamically register kubernetes context store endpoint type.
2019-05-27 20:50:39 +03:00
a720cf572f Push check for kubernetes requirement down into the endpoint
This is less of a layering violation and removes some ugly hardcoded
`"kubernetes"` strings which were needed to avoid an import loop.

Signed-off-by: Ian Campbell <ijc@docker.com>
(cherry picked from commit c455193d14)
Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
2019-05-24 16:50:21 +02:00
ec7a9ad6e4 Dynamically register kubernetes context store endpoint type.
This removes the need for the core context code to import
`github.com/docker/cli/cli/context/kubernetes` which in turn reduces the
transitive import tree in this file to not pull in all of Kubernetes.

Note that this means that any calling code which is interested in the
kubernetes endpoint must import `github.com/docker/cli/cli/context/kubernetes`
itself somewhere in order to trigger the dynamic registration. In practice
anything which is interested in Kubernetes must import that package (e.g.
`./cli/command/context.list` does for the `EndpointFromContext` function) to do
anything useful, so this restriction is not too onerous.

As a special case a small amount of Kubernetes related logic remains in
`ResolveDefaultContext` to handle error handling when the stack orchestrator
includes Kubernetes. In order to avoid a circular import loop this hardcodes
the kube endpoint name.

Similarly to avoid an import loop the existing `TestDefaultContextInitializer`
cannot continue to unit test for the Kubernetes case, so that aspect of the
test is carved off into a very similar test in the kubernetes context package.

Lastly, note that the kubernetes endpoint is now modifiable via
`WithContextEndpointType`.

Signed-off-by: Ian Campbell <ijc@docker.com>
(cherry picked from commit 520be05c49)
Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
2019-05-24 15:55:50 +02:00
5e413159e5 Export DefaultContextStoreConfig() and ResolveDefaultContext()
These are needed by any dynamically registered (via
`RegisterDefaultStoreEndpoints`) endpoint type to write a useful/sensible unit
test.

Signed-off-by: Ian Campbell <ijc@docker.com>
(cherry picked from commit f820766f6a)
Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
2019-05-24 15:55:40 +02:00
d4226d2f73 Allow dynamically registered context endpoint to provide their defaults.
Previously an endpoint registered using `RegisterDefaultStoreEndpoints` would
not be taken into consideration by `resolveDefaultContext` and so could not
provide any details.

Resolve this by passing a `store.Config` to `resolveDefaultContext` and using
it to iterate over all registered endpoints. Any endpoint can ensure that their
type implements the new `EndpointDefaultResolver` in order to provide a default.

The Docker and Kubernetes endpoints are special cased, shortly the Kubernetes
one will be refactored to be dynamically registered.

Signed-off-by: Ian Campbell <ijc@docker.com>
(cherry picked from commit 1433e27420)
Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
2019-05-24 15:55:34 +02:00
a7c10adf4e Add a helper to iterate over all endpoint types in a context store
Unused for now.

Signed-off-by: Ian Campbell <ijc@docker.com>
(cherry picked from commit 4f14c4995e)
Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
2019-05-24 15:55:24 +02:00
a4f41d94db Support dynamic registration of context store endpoint types
This is a yet unused and the default set remains the same, no expected
functional change.

Signed-off-by: Ian Campbell <ijc@docker.com>
(cherry picked from commit 087c3f7d08)
Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
2019-05-24 15:55:18 +02:00
71e1883ca0 e2e: add a test for context ls
I'm about to refactor the code which includes the Kubernetes support in a way
which relies on something vendoring `./cli/context/kubernetes/` in order to
trigger the inclusion of support for the Kubernetes endpoint in the final
binary.

In practice anything which is interested in Kubernetes must import that package
(e.g. `./cli/command/context.list` does for the `EndpointFromContext`
function). However if it was somehow possible to build without that import then
the `KUBERNETES ENDPOINT` column would be mysteriously empty.

Out of an abundance of caution add a specific check on the final binary.

Signed-off-by: Ian Campbell <ijc@docker.com>
(cherry picked from commit d5d693aa6e)
Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
2019-05-24 15:53:45 +02:00
06eb05570a fix a few typos
Signed-off-by: Ian Campbell <ijc@docker.com>
(cherry picked from commit d84e278aac)
Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
2019-05-24 15:53:36 +02:00
a1b83ffd2c Merge pull request #1894 from thaJeztah/19.03_backport_reduce_vendoring_impact
[19.03 backport] Allow vendorers of docker/cli to avoid transitively pulling in a big chunk if k8s too
2019-05-24 11:10:13 +02:00
649097ffe0 Merge pull request #1905 from thaJeztah/19.03_backport_plugin_experimental
[19.03 backport] cli-plugins: add concept of experimental plugin, only enabled in experimental mode
2019-05-23 14:05:21 -07:00
57f1de13b3 cli-plugins: add test names for easier debugging
Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit bb8e89bb2e)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-23 21:28:18 +02:00
c5431132d7 cli-plugins: add concept of experimental plugin, only enabled in experimental mode
To test, add $(pwd)/build/plugins-linux-amd64 to "cliPluginsExtraDirs" config and run:
make plugins
make binary
HELLO_EXPERIMENTAL=1 docker helloworld

To show it enabled:
HELLO_EXPERIMENTAL=1 DOCKER_CLI_EXPERIMENTAL=enabled docker helloworld

Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit 6ca8783730)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-23 21:28:09 +02:00
c66cebee7a Use underlying NewKubernetesConfig directly from compose-on-kubernetes.
The comment on `github.com/docker/cli/kubernetes.NewKubernetesConfig` said:

    // Deprecated: Use github.com/docker/compose-on-kubernetes/api.NewKubernetesConfig instead

By making this switch in `github.com/docker/cli/context/kubernetes/load.go` we
break a vendoring chain:

`github.com/docker/cli/cli/command`
→ `vendor/github.com/docker/cli/cli/context/kubernetes/load.go`
  → `vendor/github.com/docker/cli/kubernetes`
     → `github.com/docker/compose-on-kubernetes/api/compose/...`

This means that projects which just want `github.com/docker/cli/cli/command`
(which is itself pulled in transitively by
`github.com/docker/cli/cli-plugins/plugin`) which do not themselves need the
compose-on-kubernetes API avoid a huge pile of transitive dependencies.

On one of my private projects the diff on the vendor dir is:

    280 files changed, 21 insertions(+), 211346 deletions(-)

and includes dropping:

* `github.com/docker/compose-on-kubernetes/api/compose/{clone,impersonation}`
* `github.com/docker/compose-on-kubernetes/api/compose/{v1alpha3,v1beta1,v1beta2,v1beta3}`
* `github.com/google/btree`
* `github.com/googleapis/gnostic`
* `github.com/gregjones/httpcache`
* `github.com/peterbourgon/diskv`
* `k8s.io/api/*` (_lots_ of subpackages)
* `k8s.io/client-go/{discovery,kubernetes/scheme}`

and I've gone from:

    $ du -sh vendor/k8s.io/
    8.1M	vendor/k8s.io/

to:

    $ du -sh vendor/k8s.io/
    2.1M	vendor/k8s.io/

(overall I went from 36M → 29M of vendor dir for this particular project)

The change to `cli/command/system/version.go` is just for consistency and
allows us to drop the now unused alias.

Signed-off-by: Ian Campbell <ijc@docker.com>
(cherry picked from commit 8635abd662)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-20 18:14:36 +02:00
c105a58f65 rename package import kubcontext → kubecontext
The (small number) of other places which name this import use `kubecontext`,
make it consistent.

Signed-off-by: Ian Campbell <ijc@docker.com>
(cherry picked from commit 1e5129f027)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-20 18:14:27 +02:00
545fd2ad76 add containerd/ttrpc f02858b1457c5ca3aaec3a0803eb0d59f96e41d6
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 41fe464139)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 21:59:47 -07:00
315f7d7d04 bump golang.org/x/crypto 88737f569e3a9c7ab309cdc09a07fe7fc87233c3
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 51de9a883a)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 21:52:47 -07:00
6aedc5e912 bump gogo/protobuf v1.2.1
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 4de6cb0136)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 21:52:38 -07:00
3ac398aa49 bump gogo/googleapis v1.2.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 415cb3d90e)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 21:52:29 -07:00
781c427788 bump containerd/console 0650fd9eeb50bab4fc99dceb9f2e14cf58f36e7f
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 4cb01169ec)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 21:52:20 -07:00
47e66c5812 bump containerd/continuity aaeac12a7ffcd198ae25440a9dff125c2e2703a7
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit dbfeaae5eb)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 21:52:10 -07:00
9933222452 bump containerd aaeac12a7ffcd198ae25440a9dff125c2e2703a7
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 8ea94a1724)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 21:52:01 -07:00
3f5553548b vendor: bump runc v1.0.0-rc8
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit a4f01d8765)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 21:51:52 -07:00
c8273616ee bump docker/docker bff7e300e6bdb18c2417e23594bf26063a378dee (19.03 branch)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 21:51:30 -07:00
8aebc31806 Merge pull request #1884 from thaJeztah/19.03_bump_buildkit_grpc
[19.03 backport] vendor buildkit to f238f1e, bump google.golang.org/grpc v1.20.1
2019-05-13 18:56:16 -07:00
57ef4e32f4 bump google.golang.org/grpc v1.20.1
full diff: https://github.com/grpc/grpc-go/compare/v1.12.2...v1.20.1

includes  grpc/grpc-go#2695 transport: do not close channel that can lead to panic
addresses moby/moby#39053

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 93d76c5c90)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 18:43:30 -07:00
c15fb3a8e5 vendor buildkit to f238f1e
Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit 529ef6e89a)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 18:43:13 -07:00
cb07256868 Merge pull request #1882 from thaJeztah/19.03_backport_fix_powershell_codehint
[19.03 backport] Fix PowerShell codehint for rouge
2019-05-13 17:49:29 -07:00
5ec13f81a2 Fix PowerShell codehint for rouge
Rouge is case-sensitive, and only works with powershell
all lowercase.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 5331358d3e)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 17:31:56 -07:00
394c393998 Merge pull request #1878 from thaJeztah/19.03_plugin_fixes
[19.03 backport] plugin fixes
2019-05-13 14:40:08 -07:00
a4ba5831a0 Merge pull request #1875 from thaJeztah/19.03_backport_bump-golang-1.12.5
[19.03 backport] Bump golang 1.12.5
2019-05-13 14:33:46 -07:00
ac45214f7d Merge pull request #1876 from thaJeztah/19.03_backport_completion_scripts
[19.03 backport] backport bash completion scripts
2019-05-13 14:25:23 -07:00
12a1cf4783 Merge pull request #1879 from thaJeztah/19.03_backport_buildkit_fixes
[19.03 backport] backport BuildKit fixes
2019-05-13 14:24:38 -07:00
7fd21aefd8 Merge pull request #1877 from thaJeztah/19.03_backport_authors_ci_fixes
[19.03 backport] update authors and upload junit.xml
2019-05-13 18:18:20 +02:00
3f9063e775 vendor buildkit to 646fc0af6d283397b9e47cd0a18779e9d0376e0e (v0.5.1)
Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit 7f45a0e52c)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 09:13:40 -07:00
8758cdca10 build: add --platform local
Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit daca70d820)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 09:13:25 -07:00
529b1e7ec7 build: honor BUILDKIT_PROGRESS env config
Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit 8adcedd658)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 09:13:17 -07:00
b8bfba8dc6 cli-plugins: fix when plugin does not use PersistentPreRun* hooks
This regressed in 3af168c7df ("Ensure plugins can use `PersistentPreRunE`
again.") but this wasn't noticed because the helloworld test plugin has it's
own `PersistentPreRunE` which has the effect of deferring the resolution of the
global variable. In the case where the hook isn't used the variable is resolved
during `newPluginCommand` which is before the global variable was set.

Initialize the plugin command with a stub function wrapping the call to the
(global) hook, this defers resolving the variable until after it has been set,
otherwise the initial value (`nil`) is used in the struct.

Signed-off-by: Ian Campbell <ijc@docker.com>
(cherry picked from commit af200f14ed)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 09:06:08 -07:00
d6ddcdfa6a Use command.Cli instead of command.DockerCli
The linter is complaining:

    cmd/docker/docker.go:72:23⚠️ dockerCli can be github.com/docker/cli/cli/command.Cli (interfacer)

Unclear precisely which change in the preceeding commits caused it to notice
this possibility.

Signed-off-by: Ian Campbell <ijc@docker.com>
(cherry picked from commit 7d0645c5fe)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 09:05:54 -07:00
7380aae601 Include CLI plugins in help output on unknown flag.
Previously `docker --badopt` output would not include CLI plugins.

Fixes #1813

Signed-off-by: Ian Campbell <ijc@docker.com>
(cherry picked from commit 40a6cf7c47)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 09:05:45 -07:00
6a6cd35985 Hide experimental builtin commands in help output on unknown flag.
Previously `docker --badopt` would always include experimental commands even if
experimental was not enabled.

Signed-off-by: Ian Campbell <ijc@docker.com>
(cherry picked from commit 79a75da0fd)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 09:05:34 -07:00
941a493f49 Move subtests of TestGlobalHelp into actual subtests
Signed-off-by: Ian Campbell <ijc@docker.com>
(cherry picked from commit d57175aa2e)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 09:05:25 -07:00
1e275568f1 CircleCI: store junit.xml as artifact
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit dcc414be3e)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 08:59:06 -07:00
2a78b4e9a3 Update AUTHORS and mailmap
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit ffc168ed51)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 08:58:21 -07:00
8cf8fc27fa Add bash completion for events --filter node
Signed-off-by: Harald Albers <github@albersweb.de>
(cherry picked from commit c1639e1e42)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 08:51:15 -07:00
68d67f2cbf Add bash completion for context create --from
Signed-off-by: Harald Albers <github@albersweb.de>
(cherry picked from commit b55992afc6)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 08:51:04 -07:00
c1754d9e5d bash completion: add node type filter
Signed-off-by: Trapier Marshall <trapier.marshall@docker.com>
(cherry picked from commit 50a45babac)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 08:50:52 -07:00
af9b8c1be3 Add bash completion for --security-opt systempaths=unconfined
Signed-off-by: Harald Albers <github@albersweb.de>
(cherry picked from commit 1648d6c4a4)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 08:50:37 -07:00
292fc5c580 Remove deprecated storage drivers from bash completion
Signed-off-by: Harald Albers <github@albersweb.de>
(cherry picked from commit bfa43d2989)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 08:45:10 -07:00
11f5e33a90 Bump golang 1.12.5
Signed-off-by: Jintao Zhang <zhangjintao9020@gmail.com>
(cherry picked from commit c32d1de57c)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-05-13 08:28:50 -07:00
5825 changed files with 850428 additions and 631126 deletions

View File

@ -1,14 +1,6 @@
/build/
/cli/winresources/versioninfo.json
/cli/winresources/*.syso
/man/man*/
/man/vendor/
/man/go.sum
/docs/yaml/
/docs/vendor/
/docs/go.sum
profile.out
# top-level go.mod is not meant to be checked in
/go.mod
/go.sum
.dockerignore
.git
.gitignore
appveyor.yml
build
circle.yml

3
.gitattributes vendored
View File

@ -1,3 +0,0 @@
Dockerfile* linguist-language=Dockerfile
vendor.mod linguist-language=Go-Module
vendor.sum linguist-language=Go-Checksums

7
.github/CODEOWNERS vendored
View File

@ -1,6 +1,7 @@
# GitHub code owners
# See https://github.com/blog/2392-introducing-code-owners
cli/command/stack/** @silvin-lubecki @docker/runtime-owners
contrib/completion/bash/** @albers @docker/runtime-owners
docs/** @thaJeztah @docker/runtime-owners
cli/command/stack/** @silvin-lubecki
contrib/completion/bash/** @albers
contrib/completion/zsh/** @sdurrheimer
docs/** @thaJeztah

64
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,64 @@
<!--
If you are reporting a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
If you suspect your issue is a bug, please edit your issue description to
include the BUG REPORT INFORMATION shown below. If you fail to provide this
information within 7 days, we cannot debug your issue and will close it. We
will, however, reopen it if you later provide the information.
For more information about reporting issues, see
https://github.com/docker/cli/blob/master/CONTRIBUTING.md#reporting-other-issues
---------------------------------------------------
GENERAL SUPPORT INFORMATION
---------------------------------------------------
The GitHub issue tracker is for bug reports and feature requests.
General support can be found at the following locations:
- Docker Support Forums - https://forums.docker.com
- Docker Community Slack - https://dockr.ly/community
- Post a question on StackOverflow, using the Docker tag
---------------------------------------------------
BUG REPORT INFORMATION
---------------------------------------------------
Use the commands below to provide key information from your environment:
You do NOT have to include this information if this is a FEATURE REQUEST
-->
**Description**
<!--
Briefly describe the problem you are having in a few paragraphs.
-->
**Steps to reproduce the issue:**
1.
2.
3.
**Describe the results you received:**
**Describe the results you expected:**
**Additional information you deem important (e.g. issue happens only occasionally):**
**Output of `docker version`:**
```
(paste your output here)
```
**Output of `docker info`:**
```
(paste your output here)
```
**Additional environment details (AWS, VirtualBox, physical, etc.):**

View File

@ -1,146 +0,0 @@
name: Bug report
description: Create a report to help us improve!
labels:
- kind/bug
- status/0-triage
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to report a bug!
If this is a security issue report it to the [Docker Security team](mailto:security@docker.com).
- type: textarea
id: description
attributes:
label: Description
description: Give a clear and concise description of the bug
validations:
required: true
- type: textarea
id: repro
attributes:
label: Reproduce
description: Steps to reproduce the bug
placeholder: |
1. docker run ...
2. docker kill ...
3. docker rm ...
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: What is the expected behavior?
placeholder: |
E.g. "`docker rm` should remove the container and cleanup all associated data"
- type: textarea
id: version
attributes:
label: docker version
description: Output of `docker version`
render: bash
placeholder: |
Client:
Version: 20.10.17
API version: 1.41
Go version: go1.17.11
Git commit: 100c70180fde3601def79a59cc3e996aa553c9b9
Built: Mon Jun 6 21:36:39 UTC 2022
OS/Arch: linux/amd64
Context: default
Experimental: true
Server:
Engine:
Version: 20.10.17
API version: 1.41 (minimum version 1.12)
Go version: go1.17.11
Git commit: a89b84221c8560e7a3dee2a653353429e7628424
Built: Mon Jun 6 22:32:38 2022
OS/Arch: linux/amd64
Experimental: true
containerd:
Version: 1.6.6
GitCommit: 10c12954828e7c7c9b6e0ea9b0c02b01407d3ae1
runc:
Version: 1.1.2
GitCommit: a916309fff0f838eb94e928713dbc3c0d0ac7aa4
docker-init:
Version: 0.19.0
GitCommit:
validations:
required: true
- type: textarea
id: info
attributes:
label: docker info
description: Output of `docker info`
render: bash
placeholder: |
Client:
Context: default
Debug Mode: false
Plugins:
buildx: Docker Buildx (Docker Inc., 0.8.2)
compose: Docker Compose (Docker Inc., 2.6.0)
Server:
Containers: 4
Running: 2
Paused: 0
Stopped: 2
Images: 80
Server Version: 20.10.17
Storage Driver: overlay2
Backing Filesystem: xfs
Supports d_type: true
Native Overlay Diff: false
userxattr: false
Logging Driver: local
Cgroup Driver: cgroupfs
Cgroup Version: 1
Plugins:
Volume: local
Network: bridge host ipvlan macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
Swarm: inactive
Runtimes: runc io.containerd.runc.v2 io.containerd.runtime.v1.linux
Default Runtime: runc
Init Binary: docker-init
containerd version: 10c12954828e7c7c9b6e0ea9b0c02b01407d3ae1
runc version: a916309fff0f838eb94e928713dbc3c0d0ac7aa4
init version:
Security Options:
apparmor
seccomp
Profile: default
Kernel Version: 5.13.0-1031-azure
Operating System: Ubuntu 20.04.4 LTS
OSType: linux
Architecture: x86_64
CPUs: 4
Total Memory: 15.63GiB
Name: dev
ID: UC44:2RFL:7NQ5:GGFW:34O5:DYRE:CLOH:VLGZ:64AZ:GFXC:PY6H:SAHY
Docker Root Dir: /var/lib/docker
Debug Mode: true
File Descriptors: 46
Goroutines: 134
System Time: 2022-07-06T18:07:54.812439392Z
EventsListeners: 0
Registry: https://index.docker.io/v1/
Labels:
Experimental: true
Insecure Registries:
127.0.0.0/8
Live Restore Enabled: true
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional Info
description: Additional info you want to provide such as logs, system info, environment, etc.
validations:
required: false

View File

@ -1,11 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: "Contributing to Docker"
about: "Read guidelines and tips about contributing to Docker."
url: "https://github.com/docker/cli/blob/master/CONTRIBUTING.md"
- name: "Security and Vulnerabilities"
about: "Report any security issues or vulnerabilities responsibly to the Docker security team. Do not use the public issue tracker."
url: "https://github.com/moby/moby/security/policy"
- name: "General Support"
about: "Get the help you need to build, share, and run your Docker applications"
url: "https://www.docker.com/support/"

View File

@ -1,13 +0,0 @@
name: Feature request
description: Missing functionality? Come tell us about it!
labels:
- kind/feature
- status/0-triage
body:
- type: textarea
id: description
attributes:
label: Description
description: What is the feature you want to see?
validations:
required: true

View File

@ -1,5 +1,5 @@
<!--
Make sure you've read and understood our contributing guidelines;
Please make sure you've read and understood our contributing guidelines;
https://github.com/docker/cli/blob/master/CONTRIBUTING.md
** Make sure all your commits include a signature generated with `git commit -s` **
@ -10,7 +10,7 @@ guide https://docs.docker.com/opensource/code/
If this is a bug fix, make sure your description includes "fixes #xxxx", or
"closes #xxxx"
Provide the following information:
Please provide the following information:
-->
**- What I did**
@ -22,13 +22,9 @@ Provide the following information:
**- Description for the changelog**
<!--
Write a short (one line) summary that describes the changes in this
pull request for inclusion in the changelog.
It must be placed inside the below triple backticks section:
pull request for inclusion in the changelog:
-->
```markdown changelog
```
**- A picture of a cute animal (not mandatory but encouraged)**

View File

@ -1,9 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
labels:
- "area/testing"
- "status/2-code-review"

View File

@ -1,184 +0,0 @@
name: build
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
VERSION: ${{ github.ref }}
on:
workflow_dispatch:
push:
branches:
- 'master'
- '[0-9]+.[0-9]+'
tags:
- 'v*'
pull_request:
jobs:
prepare:
runs-on: ubuntu-24.04
outputs:
matrix: ${{ steps.platforms.outputs.matrix }}
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Create matrix
id: platforms
run: |
echo "matrix=$(docker buildx bake cross --print | jq -cr '.target."cross".platforms')" >>${GITHUB_OUTPUT}
-
name: Show matrix
run: |
echo ${{ steps.platforms.outputs.matrix }}
build:
runs-on: ubuntu-24.04
needs:
- prepare
strategy:
fail-fast: false
matrix:
target:
- binary
- dynbinary
platform: ${{ fromJson(needs.prepare.outputs.matrix) }}
use_glibc:
- ""
- glibc
steps:
-
name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build
uses: docker/bake-action@v5
with:
targets: ${{ matrix.target }}
set: |
*.platform=${{ matrix.platform }}
env:
USE_GLIBC: ${{ matrix.use_glibc }}
-
name: Create tarball
working-directory: ./build
run: |
mkdir /tmp/out
platform=${{ matrix.platform }}
platformPair=${platform//\//-}
tar -cvzf "/tmp/out/docker-${platformPair}.tar.gz" .
if [ -z "${{ matrix.use_glibc }}" ]; then
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}" >> $GITHUB_ENV
else
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}-glibc" >> $GITHUB_ENV
fi
-
name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ env.ARTIFACT_NAME }}
path: /tmp/out/*
if-no-files-found: error
bin-image:
runs-on: ubuntu-24.04
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: dockereng/cli-bin
tags: |
type=semver,pattern={{version}}
type=ref,event=branch
type=ref,event=pr
type=sha
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_CLIBIN_USERNAME }}
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
-
name: Build and push image
uses: docker/bake-action@v5
with:
files: |
./docker-bake.hcl
${{ steps.meta.outputs.bake-file }}
targets: bin-image-cross
push: ${{ github.event_name != 'pull_request' }}
set: |
*.cache-from=type=gha,scope=bin-image
*.cache-to=type=gha,scope=bin-image,mode=max
prepare-plugins:
runs-on: ubuntu-24.04
outputs:
matrix: ${{ steps.platforms.outputs.matrix }}
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Create matrix
id: platforms
run: |
echo "matrix=$(docker buildx bake plugins-cross --print | jq -cr '.target."plugins-cross".platforms')" >>${GITHUB_OUTPUT}
-
name: Show matrix
run: |
echo ${{ steps.platforms.outputs.matrix }}
plugins:
runs-on: ubuntu-24.04
needs:
- prepare-plugins
strategy:
fail-fast: false
matrix:
platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }}
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build
uses: docker/bake-action@v5
with:
targets: plugins-cross
set: |
*.platform=${{ matrix.platform }}

View File

@ -1,83 +0,0 @@
name: codeql
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
on:
push:
branches:
- 'master'
- '[0-9]+.[0-9]+'
tags:
- 'v*'
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12)
# │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday)
# │ │ │ │ │
# │ │ │ │ │
# │ │ │ │ │
# * * * * *
- cron: '0 9 * * 4'
jobs:
codeql:
runs-on: 'ubuntu-latest'
timeout-minutes: 360
env:
DISABLE_WARN_OUTSIDE_CONTAINER: '1'
permissions:
actions: read
contents: read
security-events: write
steps:
-
name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
-
name: Checkout HEAD on PR
if: ${{ github.event_name == 'pull_request' }}
run: |
git checkout HEAD^2
# CodeQL 2.16.4's auto-build added support for multi-module repositories,
# and is trying to be smart by searching for modules in every directory,
# including vendor directories. If no module is found, it's creating one
# which is ... not what we want, so let's give it a "go.mod".
# see: https://github.com/docker/cli/pull/4944#issuecomment-2002034698
-
name: Create go.mod
run: |
ln -s vendor.mod go.mod
ln -s vendor.sum go.sum
-
name: Update Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
-
name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: go
-
name: Autobuild
uses: github/codeql-action/autobuild@v3
-
name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:go"

View File

@ -1,81 +0,0 @@
name: e2e
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
push:
branches:
- 'master'
- '[0-9]+.[0-9]+'
tags:
- 'v*'
pull_request:
jobs:
e2e:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
target:
- non-experimental
- experimental
- connhelper-ssh
base:
- alpine
- debian
engine-version:
- 27.0 # latest
- 26.1 # latest - 1
- 23.0 # mirantis lts
# TODO(krissetto) 19.03 needs a look, doesn't work ubuntu 22.04 (cgroup errors).
# we could have a separate job that tests it against ubuntu 20.04
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Update daemon.json
run: |
if [ ! -f /etc/docker/daemon.json ]; then
# ubuntu 24.04 runners no longer have a default daemon.json present
sudo mkdir -p /etc/docker/
echo '{"experimental": true}' | sudo tee /etc/docker/daemon.json
else
# but if there is one; let's patch it to keep other options that may be set.
sudo jq '.experimental = true' < /etc/docker/daemon.json > /tmp/docker.json
sudo mv /tmp/docker.json /etc/docker/daemon.json
fi
sudo cat /etc/docker/daemon.json
sudo service docker restart
docker version
docker info
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Run ${{ matrix.target }}
run: |
make -f docker.Makefile test-e2e-${{ matrix.target }}
env:
BASE_VARIANT: ${{ matrix.base }}
ENGINE_VERSION: ${{ matrix.engine-version }}
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
-
name: Send to Codecov
uses: codecov/codecov-action@v4
with:
file: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -1,91 +0,0 @@
name: test
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
push:
branches:
- 'master'
- '[0-9]+.[0-9]+'
tags:
- 'v*'
pull_request:
jobs:
ctn:
runs-on: ubuntu-24.04
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Test
uses: docker/bake-action@v5
with:
targets: test-coverage
-
name: Send to Codecov
uses: codecov/codecov-action@v4
with:
file: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
host:
runs-on: ${{ matrix.os }}
env:
GOPATH: ${{ github.workspace }}
GOBIN: ${{ github.workspace }}/bin
GO111MODULE: auto
strategy:
fail-fast: false
matrix:
os:
- 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: Prepare git
if: matrix.os == 'windows-latest'
run: |
git config --system core.autocrlf false
git config --system core.eol lf
-
name: Checkout
uses: actions/checkout@v4
with:
path: ${{ env.GOPATH }}/src/github.com/docker/cli
-
name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.22.7
-
name: Test
run: |
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
-
name: Send to Codecov
uses: codecov/codecov-action@v4
with:
file: /tmp/coverage.txt
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -1,77 +0,0 @@
name: validate-pr
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
on:
pull_request:
types: [opened, edited, labeled, unlabeled]
jobs:
check-area-label:
runs-on: ubuntu-20.04
steps:
- name: Missing `area/` label
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: OK
run: exit 0
check-changelog:
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/')
runs-on: ubuntu-20.04
env:
PR_BODY: |
${{ github.event.pull_request.body }}
steps:
- name: Check changelog description
run: |
# Extract the `markdown changelog` note code block
block=$(echo -n "$PR_BODY" | tr -d '\r' | awk '/^```markdown changelog$/{flag=1;next}/^```$/{flag=0}flag')
# Strip empty lines
desc=$(echo "$block" | awk NF)
if [ -z "$desc" ]; then
echo "::error::Changelog section is empty. Provide a description for the changelog."
exit 1
fi
len=$(echo -n "$desc" | wc -c)
if [[ $len -le 6 ]]; then
echo "::error::Description looks too short: $desc"
exit 1
fi
echo "This PR will be included in the release notes with the following note:"
echo "$desc"
check-pr-branch:
runs-on: ubuntu-20.04
env:
PR_TITLE: ${{ github.event.pull_request.title }}
steps:
# Backports or PR that target a release branch directly should mention the target branch in the title, for example:
# [X.Y backport] Some change that needs backporting to X.Y
# [X.Y] Change directly targeting the X.Y branch
- name: Check release branch
id: title_branch
run: |
# get the intended major version prefix ("[27.1 backport]" -> "27.") from the PR title.
[[ "$PR_TITLE" =~ ^\[([0-9]*\.)[^]]*\] ]] && branch="${BASH_REMATCH[1]}"
# get major version prefix from the release branch ("27.x -> "27.")
[[ "$GITHUB_BASE_REF" =~ ^([0-9]*\.) ]] && target_branch="${BASH_REMATCH[1]}" || target_branch="$GITHUB_BASE_REF"
if [[ "$target_branch" != "$branch" ]] && ! [[ "$GITHUB_BASE_REF" == "master" && "$branch" == "" ]]; then
echo "::error::PR is opened against the $GITHUB_BASE_REF branch, but its title suggests otherwise."
exit 1
fi

View File

@ -1,84 +0,0 @@
name: validate
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
push:
branches:
- 'master'
- '[0-9]+.[0-9]+'
tags:
- 'v*'
pull_request:
jobs:
validate:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
target:
- lint
- shellcheck
- validate-vendor
- update-authors # ensure authors update target runs fine
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Run
uses: docker/bake-action@v5
with:
targets: ${{ matrix.target }}
# check that the generated Markdown and the checked-in files match
validate-md:
runs-on: ubuntu-24.04
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Generate
shell: 'script --return --quiet --command "bash {0}"'
run: |
make -f docker.Makefile mddocs
-
name: Validate
run: |
if [[ $(git diff --stat) != '' ]]; then
echo 'fail: generated files do not match checked-in files'
git --no-pager diff
exit 1
fi
validate-make:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
target:
- yamldocs # ensure yamldocs target runs fine
- manpages # ensure manpages target runs fine
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Run
shell: 'script --return --quiet --command "bash {0}"'
run: |
make -f docker.Makefile ${{ matrix.target }}

15
.gitignore vendored
View File

@ -1,5 +1,5 @@
# if you want to ignore files created by your editor/tools,
# consider a global .gitignore https://help.github.com/articles/ignoring-files
# please consider a global .gitignore https://help.github.com/articles/ignoring-files
*.exe
*.exe~
*.orig
@ -8,10 +8,11 @@
Thumbs.db
.editorconfig
/build/
/cli/winresources/versioninfo.json
/cli/winresources/*.syso
cli/winresources/rsrc_386.syso
cli/winresources/rsrc_amd64.syso
/man/man1/
/man/man5/
/man/man8/
/docs/yaml/gen/
coverage.txt
profile.out
# top-level go.mod is not meant to be checked in
/go.mod
/go.sum

View File

@ -1,187 +0,0 @@
linters:
enable:
- bodyclose
- depguard
- dogsled
- dupword # Detects duplicate words.
- durationcheck
- errchkjson
- exportloopref # Detects pointers to enclosing loop variables.
- gocritic # Metalinter; detects bugs, performance, and styling issues.
- gocyclo
- gofumpt # Detects whether code was gofumpt-ed.
- goimports
- gosec # Detects security problems.
- gosimple
- govet
- ineffassign
- lll
- megacheck
- misspell # Detects commonly misspelled English words in comments.
- nakedret
- nilerr # Detects code that returns nil even if it checks that the error is not nil.
- nolintlint # Detects ill-formed or insufficient nolint directives.
- perfsprint # Detects fmt.Sprintf uses that can be replaced with a faster alternative.
- prealloc # Detects slice declarations that could potentially be pre-allocated.
- predeclared # Detects code that shadows one of Go's predeclared identifiers
- reassign
- revive # Metalinter; drop-in replacement for golint.
- staticcheck
- stylecheck # Replacement for golint
- tenv # Detects using os.Setenv instead of t.Setenv.
- thelper # Detects test helpers without t.Helper().
- tparallel # Detects inappropriate usage of t.Parallel().
- typecheck
- unconvert # Detects unnecessary type conversions.
- unparam
- unused
- usestdlibvars
- vet
- wastedassign
disable:
- errcheck
run:
timeout: 5m
linters-settings:
depguard:
rules:
main:
deny:
- pkg: io/ioutil
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
gocyclo:
min-complexity: 16
govet:
enable:
- shadow
settings:
shadow:
strict: true
lll:
line-length: 200
nakedret:
command: nakedret
pattern: ^(?P<path>.*?\\.go):(?P<line>\\d+)\\s*(?P<message>.*)$
revive:
rules:
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing
- name: import-shadowing
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block
- name: empty-block
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
- name: empty-lines
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
- name: use-any
severity: warning
disabled: false
issues:
# The default exclusion rules are a bit too permissive, so copying the relevant ones below
exclude-use-default: false
exclude:
- parameter .* always receives
exclude-files:
- cli/compose/schema/bindata.go
- .*generated.*
exclude-rules:
# We prefer to use an "exclude-list" so that new "default" exclusions are not
# automatically inherited. We can decide whether or not to follow upstream
# defaults when updating golang-ci-lint versions.
# Unfortunately, this means we have to copy the whole exclusion pattern, as
# (unlike the "include" option), the "exclude" option does not take exclusion
# ID's.
#
# These exclusion patterns are copied from the default excluses at:
# https://github.com/golangci/golangci-lint/blob/v1.44.0/pkg/config/issues.go#L10-L104
# EXC0001
- text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv). is not checked"
linters:
- errcheck
# EXC0003
- text: "func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this"
linters:
- revive
# EXC0006
- text: "Use of unsafe calls should be audited"
linters:
- gosec
# EXC0007
- text: "Subprocess launch(ed with variable|ing should be audited)"
linters:
- gosec
# EXC0008
# TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close" (gosec)
- text: "G307"
linters:
- gosec
# EXC0009
- text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
linters:
- gosec
# EXC0010
- text: "Potential file inclusion via variable"
linters:
- gosec
# G113 Potential uncontrolled memory consumption in Rat.SetString (CVE-2022-23772)
# only affects gp < 1.16.14. and go < 1.17.7
- text: "G113"
linters:
- gosec
# TODO: G104: Errors unhandled. (gosec)
- text: "G104"
linters:
- gosec
# Looks like the match in "EXC0007" above doesn't catch this one
# TODO: consider upstreaming this to golangci-lint's default exclusion rules
- text: "G204: Subprocess launched with a potential tainted input or cmd arguments"
linters:
- gosec
# Looks like the match in "EXC0009" above doesn't catch this one
# TODO: consider upstreaming this to golangci-lint's default exclusion rules
- text: "G306: Expect WriteFile permissions to be 0600 or less"
linters:
- gosec
# TODO: make sure all packages have a description. Currently, there's 67 packages without.
- text: "package-comments: should have a package comment"
linters:
- revive
# FIXME temporarily suppress these (see https://github.com/gotestyourself/gotest.tools/issues/272)
- text: "SA1019: (assert|cmp|is)\\.ErrorType is deprecated"
linters:
- staticcheck
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- errcheck
- gosec
- text: "ST1000: at least one file in a package should have a package comment"
linters:
- stylecheck
# Allow "err" and "ok" vars to shadow existing declarations, otherwise we get too many false positives.
- text: '^shadow: declaration of "(err|ok)" shadows declaration'
linters:
- govet
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-issues-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0

162
.mailmap
View File

@ -6,27 +6,16 @@
#
# For explanation on this file format: man git-shortlog
<lmscrewy@gmail.com> <1674195+squeegels@users.noreply.github.com>
Aaron L. Xu <liker.xu@foxmail.com>
Aaron Lehmann <alehmann@netflix.com>
Aaron Lehmann <alehmann@netflix.com> <aaron.lehmann@docker.com>
Abhinandan Prativadi <abhi@docker.com>
Ace Tang <aceapril@126.com>
Adrien Gallouët <adrien@gallouet.fr> <angt@users.noreply.github.com>
Ahmed Kamal <email.ahmedkamal@googlemail.com>
Ahmet Alp Balkan <ahmetb@microsoft.com> <ahmetalpbalkan@gmail.com>
AJ Bowen <aj@soulshake.net>
AJ Bowen <aj@soulshake.net> <aj@gandi.net>
AJ Bowen <aj@soulshake.net> <amy@gandi.net>
AJ Bowen <aj@gandi.net>
AJ Bowen <aj@gandi.net> <amy@gandi.net>
Akihiro Matsushima <amatsusbit@gmail.com> <amatsus@users.noreply.github.com>
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.akihiro@lab.ntt.co.jp>
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.kyoto@gmail.com>
Alano Terblanche <alano.terblanche@docker.com>
Alano Terblanche <alano.terblanche@docker.com> <18033717+Benehiko@users.noreply.github.com>
Albin Kerouanton <albinker@gmail.com>
Albin Kerouanton <albinker@gmail.com> <557933+akerouanton@users.noreply.github.com>
Albin Kerouanton <albinker@gmail.com> <albin@akerouanton.name>
Akihiro Suda <suda.akihiro@lab.ntt.co.jp> <suda.kyoto@gmail.com>
Aleksa Sarai <asarai@suse.de>
Aleksa Sarai <asarai@suse.de> <asarai@suse.com>
Aleksa Sarai <asarai@suse.de> <cyphar@cyphar.com>
@ -34,12 +23,10 @@ Aleksandrs Fadins <aleks@s-ko.net>
Alessandro Boch <aboch@tetrationanalytics.com> <aboch@docker.com>
Alex Chen <alexchenunix@gmail.com> <root@localhost.localdomain>
Alex Ellis <alexellis2@gmail.com>
Alexander Chneerov <achneerov@gmail.com>
Alexander Larsson <alexl@redhat.com> <alexander.larsson@gmail.com>
Alexander Morozov <lk4d4math@gmail.com>
Alexander Morozov <lk4d4math@gmail.com> <lk4d4@docker.com>
Alexander Morozov <lk4d4@docker.com>
Alexander Morozov <lk4d4@docker.com> <lk4d4math@gmail.com>
Alexandre Beslic <alexandre.beslic@gmail.com> <abronan@docker.com>
Alexis Couvreur <alexiscouvreur.pro@gmail.com>
Alicia Lauerman <alicia@eta.im> <allydevour@me.com>
Allen Sun <allensun.shl@alibaba-inc.com> <allen.sun@daocloud.io>
Allen Sun <allensun.shl@alibaba-inc.com> <shlallen1990@gmail.com>
@ -58,18 +45,13 @@ Anuj Bahuguna <anujbahuguna.dev@gmail.com>
Anuj Bahuguna <anujbahuguna.dev@gmail.com> <abahuguna@fiberlink.com>
Anusha Ragunathan <anusha.ragunathan@docker.com> <anusha@docker.com>
Ao Li <la9249@163.com>
Arko Dasgupta <arko@tetrate.io>
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>
Arnaud Porterie <arnaud.porterie@docker.com>
Arnaud Porterie <arnaud.porterie@docker.com> <icecrime@gmail.com>
Arthur Gautier <baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net>
Arthur Peka <arthur.peka@outlook.com> <arthrp@users.noreply.github.com>
Avi Miller <avi.miller@oracle.com> <avi.miller@gmail.com>
Ben Bonnefoy <frenchben@docker.com>
Ben Golub <ben.golub@dotcloud.com>
Ben Toews <mastahyeti@gmail.com> <mastahyeti@users.noreply.github.com>
Benjamin Nater <me@bn4t.me>
Benoit Chesneau <bchesneau@gmail.com>
Bhiraj Butala <abhiraj.butala@gmail.com>
Bhumika Bayani <bhumikabayani@gmail.com>
@ -78,21 +60,13 @@ Bill Wang <ozbillwang@gmail.com> <SydOps@users.noreply.github.com>
Bin Liu <liubin0329@gmail.com>
Bin Liu <liubin0329@gmail.com> <liubin0329@users.noreply.github.com>
Bingshen Wang <bingshen.wbs@alibaba-inc.com>
Bjorn Neergaard <bjorn.neergaard@docker.com>
Bjorn Neergaard <bjorn.neergaard@docker.com> <bjorn@neersighted.com>
Bjorn Neergaard <bjorn.neergaard@docker.com> <bneergaard@mirantis.com>
Boaz Shuster <ripcurld.github@gmail.com>
Brad Baker <brad@brad.fi>
Brad Baker <brad@brad.fi> <88946291+brdbkr@users.noreply.github.com>
Brandon Philips <brandon.philips@coreos.com> <brandon@ifup.co>
Brandon Philips <brandon.philips@coreos.com> <brandon@ifup.org>
Brent Salisbury <brent.salisbury@docker.com> <brent@docker.com>
Brian Goff <cpuguy83@gmail.com>
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.home>
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.local>
Brian Tracy <brian.tracy33@gmail.com>
Calvin Liu <flycalvin@qq.com>
Carlos de Paula <me@carlosedp.com>
Chad Faragher <wyckster@hotmail.com>
Chander Govindarajan <chandergovind@gmail.com>
Chao Wang <wangchao.fnst@cn.fujitsu.com> <chaowang@localhost.localdomain>
@ -102,19 +76,13 @@ Chen Chuanliang <chen.chuanliang@zte.com.cn>
Chen Mingjie <chenmingjie0828@163.com>
Chen Qiu <cheney-90@hotmail.com>
Chen Qiu <cheney-90@hotmail.com> <21321229@zju.edu.cn>
Chris Chinchilla <chris@chrischinchilla.com>
Chris Chinchilla <chris@chrischinchilla.com> <chris.ward@docker.com>
Chris Dias <cdias@microsoft.com>
Chris McKinnel <chris.mckinnel@tangentlabs.co.uk>
Christopher Biscardi <biscarch@sketcht.com>
Christopher Crone <christopher.crone@docker.com>
Christopher Latham <sudosurootdev@gmail.com>
Christy Norman <christy@linux.vnet.ibm.com>
Chun Chen <ramichen@tencent.com> <chenchun.feed@gmail.com>
Comical Derskeal <27731088+derskeal@users.noreply.github.com>
Corbin Coleman <corbin.coleman@docker.com>
Cory Bennet <cbennett@netflix.com>
Craig Osterhout <craig.osterhout@docker.com>
Cristian Staretu <cristian.staretu@gmail.com>
Cristian Staretu <cristian.staretu@gmail.com> <unclejack@users.noreply.github.com>
Cristian Staretu <cristian.staretu@gmail.com> <unclejacksons@gmail.com>
@ -122,9 +90,7 @@ CUI Wei <ghostplant@qq.com> cuiwei13 <cuiwei13@pku.edu.cn>
Daehyeok Mun <daehyeok@gmail.com>
Daehyeok Mun <daehyeok@gmail.com> <daehyeok@daehyeok-ui-MacBook-Air.local>
Daehyeok Mun <daehyeok@gmail.com> <daehyeok@daehyeokui-MacBook-Air.local>
Daisuke Ito <itodaisuke00@gmail.com>
Dan Feldman <danf@jfrog.com>
Danial Gharib <danial.mail.gh@gmail.com>
Daniel Dao <dqminh@cloudflare.com>
Daniel Dao <dqminh@cloudflare.com> <dqminh89@gmail.com>
Daniel Garcia <daniel@danielgarcia.info>
@ -144,27 +110,17 @@ Dattatraya Kumbhar <dattatraya.kumbhar@gslab.com>
Dave Goodchild <buddhamagnet@gmail.com>
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 Karlsson <david.karlsson@docker.com>
David Karlsson <david.karlsson@docker.com> <35727626+dvdksn@users.noreply.github.com>
David M. Karr <davidmichaelkarr@gmail.com>
David Sheets <dsheets@docker.com> <sheets@alum.mit.edu>
David Sissitka <me@dsissitka.com>
David Williamson <david.williamson@docker.com> <davidwilliamson@users.noreply.github.com>
Derek McGowan <derek@mcg.dev>
Derek McGowan <derek@mcg.dev> <derek@mcgstyle.net>
Deshi Xiao <dxiao@redhat.com> <dsxiao@dataman-inc.com>
Deshi Xiao <dxiao@redhat.com> <xiaods@gmail.com>
Diego Siqueira <dieg0@live.com>
Diogo Monica <diogo@docker.com> <diogo.monica@gmail.com>
Dominik Braun <dominik.braun@nbsp.de>
Dominik Braun <dominik.braun@nbsp.de> <Dominik.Braun@nbsp.de>
Dominik Honnef <dominik@honnef.co> <dominikh@fork-bomb.org>
Doug Davis <dug@us.ibm.com> <duglin@users.noreply.github.com>
Doug Tangren <d.tangren@gmail.com>
Drew Erny <derny@mirantis.com>
Drew Erny <derny@mirantis.com> <drew.erny@docker.com>
Elan Ruusamäe <glen@pld-linux.org>
Elan Ruusamäe <glen@pld-linux.org> <glen@delfi.ee>
Elango Sivanandam <elango.siva@docker.com>
@ -188,8 +144,6 @@ Fengtu Wang <wangfengtu@huawei.com> <wangfengtu@huawei.com>
Francisco Carriedo <fcarriedo@gmail.com>
Frank Rosquin <frank.rosquin+github@gmail.com> <frank.rosquin@gmail.com>
Frederick F. Kautz IV <fkautz@redhat.com> <fkautz@alumni.cmu.edu>
Gabriel Gore <gabgore@cisco.com>
Gabriel Gore <gabgore@cisco.com> <gabriel59kg@gmail.com>
Gabriel Nicolas Avellaneda <avellaneda.gabriel@gmail.com>
Gaetan de Villele <gdevillele@gmail.com>
Gang Qiao <qiaohai8866@gmail.com> <1373319223@qq.com>
@ -198,8 +152,6 @@ Gerwim Feiken <g.feiken@tfe.nl> <gerwim@gmail.com>
Giampaolo Mancini <giampaolo@trampolineup.com>
Gopikannan Venugopalsamy <gopikannan.venugopalsamy@gmail.com>
Gou Rao <gou@portworx.com> <gourao@users.noreply.github.com>
Graeme Wiebe <graeme.wiebe@gmail.com>
Graeme Wiebe <graeme.wiebe@gmail.com> <79593869+TheRealGramdalf@users.noreply.github.com>
Greg Stephens <greg@udon.org>
Guillaume J. Charmes <guillaume.charmes@docker.com> <charmes.guillaume@gmail.com>
Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume.charmes@dotcloud.com>
@ -207,7 +159,6 @@ Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume@charmes.net>
Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume@docker.com>
Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume@dotcloud.com>
Guillaume Le Floch <glfloch@gmail.com>
Guillaume Tardif <guillaume.tardif@gmail.com>
Gurjeet Singh <gurjeet@singh.im> <singh.gurjeet@gmail.com>
Gustav Sinder <gustav.sinder@gmail.com>
Günther Jungbluth <gunther@gameslabs.net>
@ -226,7 +177,6 @@ Hollie Teal <hollie@docker.com>
Hollie Teal <hollie@docker.com> <hollie.teal@docker.com>
Hollie Teal <hollie@docker.com> <hollietealok@users.noreply.github.com>
Hu Keping <hukeping@huawei.com>
Hugo Gabriel Eyherabide <hugogabriel.eyherabide@gmail.com>
Huu Nguyen <huu@prismskylabs.com> <whoshuu@gmail.com>
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com>
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com> <1187766782@qq.com>
@ -240,7 +190,6 @@ Jaivish Kothari <janonymous.codevulture@gmail.com>
Jake Lambert <jake.lambert@volusion.com>
Jake Lambert <jake.lambert@volusion.com> <32850427+jake-lambert-volusion@users.noreply.github.com>
Jamie Hannaford <jamie@limetree.org> <jamie.hannaford@rackspace.com>
Jean Lecordier <jeanlecordier@hotmail.fr>
Jean-Baptiste Barth <jeanbaptiste.barth@gmail.com>
Jean-Baptiste Dalido <jeanbaptiste@appgratis.com>
Jean-Pierre Huynh <jean-pierre.huynh@ounet.fr> <jp@moogsoft.com>
@ -248,17 +197,14 @@ Jean-Tiare Le Bigot <jt@yadutaf.fr> <admin@jtlebi.fr>
Jeff Anderson <jeff@docker.com> <jefferya@programmerq.net>
Jeff Nickoloff <jeff.nickoloff@gmail.com> <jeff@allingeek.com>
Jeroen Franse <jeroenfranse@gmail.com>
Jessica Frazelle <jess@oxide.computer>
Jessica Frazelle <jess@oxide.computer> <acidburn@docker.com>
Jessica Frazelle <jess@oxide.computer> <acidburn@google.com>
Jessica Frazelle <jess@oxide.computer> <acidburn@microsoft.com>
Jessica Frazelle <jess@oxide.computer> <jess@docker.com>
Jessica Frazelle <jess@oxide.computer> <jess@mesosphere.com>
Jessica Frazelle <jess@oxide.computer> <jessfraz@google.com>
Jessica Frazelle <jess@oxide.computer> <jfrazelle@users.noreply.github.com>
Jessica Frazelle <jess@oxide.computer> <me@jessfraz.com>
Jessica Frazelle <jess@oxide.computer> <princess@docker.com>
Jim Chen <njucjc@gmail.com>
Jessica Frazelle <jessfraz@google.com>
Jessica Frazelle <jessfraz@google.com> <acidburn@docker.com>
Jessica Frazelle <jessfraz@google.com> <acidburn@google.com>
Jessica Frazelle <jessfraz@google.com> <jess@docker.com>
Jessica Frazelle <jessfraz@google.com> <jess@mesosphere.com>
Jessica Frazelle <jessfraz@google.com> <jfrazelle@users.noreply.github.com>
Jessica Frazelle <jessfraz@google.com> <me@jessfraz.com>
Jessica Frazelle <jessfraz@google.com> <princess@docker.com>
Jim Galasyn <jim.galasyn@docker.com>
Jiuyue Ma <majiuyue@huawei.com>
Joey Geiger <jgeiger@gmail.com>
@ -267,21 +213,16 @@ Joffrey F <joffrey@docker.com> <f.joffrey@gmail.com>
Joffrey F <joffrey@docker.com> <joffrey@dotcloud.com>
Johan Euphrosine <proppy@google.com> <proppy@aminche.com>
John Harris <john@johnharris.io>
John Howard <github@lowenna.com>
John Howard <github@lowenna.com> <jhoward@microsoft.com>
John Howard <github@lowenna.com> <jhoward@ntdev.microsoft.com>
John Howard <github@lowenna.com> <jhowardmsft@users.noreply.github.com>
John Howard <github@lowenna.com> <John.Howard@microsoft.com>
John Howard <github@lowenna.com> <john.howard@microsoft.com>
John Howard (VM) <John.Howard@microsoft.com>
John Howard (VM) <John.Howard@microsoft.com> <jhoward@microsoft.com>
John Howard (VM) <John.Howard@microsoft.com> <jhoward@ntdev.microsoft.com>
John Howard (VM) <John.Howard@microsoft.com> <jhowardmsft@users.noreply.github.com>
John Howard (VM) <John.Howard@microsoft.com> <john.howard@microsoft.com>
John Stephens <johnstep@docker.com> <johnstep@users.noreply.github.com>
Jonathan A. Sternberg <jonathansternberg@gmail.com>
Jonathan A. Sternberg <jonathansternberg@gmail.com> <jonathan.sternberg@docker.com>
Jordan Arentsen <blissdev@gmail.com>
Jordan Jennings <jjn2009@gmail.com> <jjn2009@users.noreply.github.com>
Jorit Kleine-Möllhoff <joppich@bricknet.de> <joppich@users.noreply.github.com>
Jose Diaz-Gonzalez <email@josediazgonzalez.com>
Jose Diaz-Gonzalez <email@josediazgonzalez.com> <jose@seatgeek.com>
Jose Diaz-Gonzalez <email@josediazgonzalez.com> <josegonzalez@users.noreply.github.com>
Jose Diaz-Gonzalez <jose@seatgeek.com> <josegonzalez@users.noreply.github.com>
Josh Eveleth <joshe@opendns.com> <jeveleth@users.noreply.github.com>
Josh Hawn <josh.hawn@docker.com> <jlhawn@berkeley.edu>
Josh Horwitz <horwitz@addthis.com> <horwitzja@gmail.com>
@ -303,20 +244,12 @@ Kai Qiang Wu (Kennan) <wkq5325@gmail.com> <wkqwu@cn.ibm.com>
Kamil Domański <kamil@domanski.co>
Kamjar Gerami <kami.gerami@gmail.com>
Kat Samperi <kat.samperi@gmail.com> <kizzie@users.noreply.github.com>
Kathryn Spiers <kathryn@spiers.me>
Kathryn Spiers <kathryn@spiers.me> <kyle@Spiers.me>
Kathryn Spiers <kathryn@spiers.me> <Kyle@Spiers.me>
Kelton Bassingthwaite <KeltonBassingthwaite@gmail.com>
Kelton Bassingthwaite <KeltonBassingthwaite@gmail.com> <github@bassingthwaite.org>
Ken Cochrane <kencochrane@gmail.com> <KenCochrane@gmail.com>
Ken Herner <kherner@progress.com> <chosenken@gmail.com>
Kenfe-Mickaël Laventure <mickael.laventure@gmail.com>
Kevin Alvarez <github@crazymax.dev>
Kevin Alvarez <github@crazymax.dev> <crazy-max@users.noreply.github.com>
Kevin Feyrer <kevin.feyrer@btinternet.com> <kevinfeyrer@users.noreply.github.com>
Kevin Kern <kaiwentan@harmonycloud.cn>
Kevin Meredith <kevin.m.meredith@gmail.com>
Kevin Woblick <mail@kovah.de>
Kir Kolyshkin <kolyshkin@gmail.com>
Kir Kolyshkin <kolyshkin@gmail.com> <kir@openvz.org>
Kir Kolyshkin <kolyshkin@gmail.com> <kolyshkin@users.noreply.github.com>
@ -325,11 +258,11 @@ Konstantin Gribov <grossws@gmail.com>
Konstantin Pelykh <kpelykh@zettaset.com>
Kotaro Yoshimatsu <kotaro.yoshimatsu@gmail.com>
Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp> <kunal.kushwaha@gmail.com>
Kyle Mitofsky <Kylemit@gmail.com>
Kyle Spiers <kyle@spiers.me>
Kyle Spiers <kyle@spiers.me> <Kyle@Spiers.me>
Lajos Papp <lajos.papp@sequenceiq.com> <lalyos@yahoo.com>
Lei Jitang <leijitang@huawei.com>
Lei Jitang <leijitang@huawei.com> <leijitang@gmail.com>
Li Fu Bang <lifubang@acmcoder.com>
Liang Mingqiang <mqliang.zju@gmail.com>
Liang-Chi Hsieh <viirya@gmail.com>
Liao Qingwei <liaoqingwei@huawei.com>
@ -345,8 +278,6 @@ Lyn <energylyn@zju.edu.cn>
Lynda O'Leary <lyndaoleary29@gmail.com>
Lynda O'Leary <lyndaoleary29@gmail.com> <lyndaoleary@hotmail.com>
Ma Müller <mueller-ma@users.noreply.github.com>
Maciej Kalisz <maciej.d.kalisz@gmail.com>
Maciej Kalisz <maciej.d.kalisz@gmail.com> <mdkalish@users.noreply.github.com>
Madhan Raj Mookkandy <MadhanRaj.Mookkandy@microsoft.com> <madhanm@microsoft.com>
Madhu Venugopal <madhu@socketplane.io> <madhu@docker.com>
Mageee <fangpuyi@foxmail.com> <21521230.zju.edu.cn>
@ -354,10 +285,8 @@ Mansi Nahar <mmn4185@rit.edu> <mansi.nahar@macbookpro-mansinahar.local>
Mansi Nahar <mmn4185@rit.edu> <mansinahar@users.noreply.github.com>
Marc Abramowitz <marc@marc-abramowitz.com> <msabramo@gmail.com>
Marcelo Horacio Fortino <info@fortinux.com> <fortinux@users.noreply.github.com>
Marco Spiess <marco.spiess@hotmail.de>
Marcus Linke <marcus.linke@gmx.de>
Marianna Tessel <mtesselh@gmail.com>
Marius Ileana <marius.ileana@gmail.com>
Mark Oates <fl0yd@me.com>
Markan Patel <mpatel678@gmail.com>
Markus Kortlang <hyp3rdino@googlemail.com> <markus.kortlang@lhsystems.com>
@ -377,11 +306,9 @@ Matthew Mosesohn <raytrac3r@gmail.com>
Matthew Mueller <mattmuelle@gmail.com>
Matthias Kühnle <git.nivoc@neverbox.com> <kuehnle@online.de>
Mauricio Garavaglia <mauricio@medallia.com> <mauriciogaravaglia@gmail.com>
Michael Crosby <crosbymichael@gmail.com>
Michael Crosby <crosbymichael@gmail.com> <crosby.michael@gmail.com>
Michael Crosby <crosbymichael@gmail.com> <michael@crosbymichael.com>
Michael Crosby <crosbymichael@gmail.com> <michael@docker.com>
Michael Crosby <crosbymichael@gmail.com> <michael@thepasture.io>
Michael Crosby <michael@docker.com> <crosby.michael@gmail.com>
Michael Crosby <michael@docker.com> <crosbymichael@gmail.com>
Michael Crosby <michael@docker.com> <michael@crosbymichael.com>
Michael Hudson-Doyle <michael.hudson@canonical.com> <michael.hudson@linaro.org>
Michael Huettermann <michael@huettermann.net>
Michael Käufl <docker@c.michael-kaeufl.de> <michael-k@users.noreply.github.com>
@ -391,15 +318,11 @@ Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com> <30386061+doncicuto@users.n
Miguel Angel Fernández <elmendalerenda@gmail.com>
Mihai Borobocea <MihaiBorob@gmail.com> <MihaiBorobocea@gmail.com>
Mike Casas <mkcsas0@gmail.com> <mikecasas@users.noreply.github.com>
Mike Dalton <mikedalton@github.com>
Mike Dalton <mikedalton@github.com> <19153140+mikedalton@users.noreply.github.com>
Mike Goelzer <mike.goelzer@docker.com> <mgoelzer@docker.com>
Milind Chawre <milindchawre@gmail.com>
Misty Stanley-Jones <misty@docker.com> <misty@apache.org>
Mohit Soni <mosoni@ebay.com> <mohitsoni1989@gmail.com>
Moorthy RS <rsmoorthy@gmail.com> <rsmoorthy@users.noreply.github.com>
Morten Hekkvang <morten.hekkvang@sbab.se>
Morten Hekkvang <morten.hekkvang@sbab.se> <morten.hekkvang@tele2.com>
Moysés Borges <moysesb@gmail.com>
Moysés Borges <moysesb@gmail.com> <moyses.furtado@wplex.com.br>
Nace Oroz <orkica@gmail.com>
@ -407,60 +330,42 @@ Nathan LeClaire <nathan.leclaire@docker.com> <nathan.leclaire@gmail.com>
Nathan LeClaire <nathan.leclaire@docker.com> <nathanleclaire@gmail.com>
Neil Horman <nhorman@tuxdriver.com> <nhorman@hmswarspite.think-freely.org>
Nick Russo <nicholasjamesrusso@gmail.com> <nicholasrusso@icloud.com>
Nick Santos <nick.santos@docker.com>
Nick Santos <nick.santos@docker.com> <nick@tilt.dev>
Nicolas Borboën <ponsfrilus@gmail.com> <ponsfrilus@users.noreply.github.com>
Nicolas De Loof <nicolas.deloof@gmail.com>
Nigel Poulton <nigelpoulton@hotmail.com>
Nik Nyby <nikolas@gnu.org> <nnyby@columbia.edu>
Nolan Darilek <nolan@thewordnerd.info>
O.S. Tezer <ostezer@gmail.com>
O.S. Tezer <ostezer@gmail.com> <ostezer@users.noreply.github.com>
Oh Jinkyun <tintypemolly@gmail.com> <tintypemolly@Ohui-MacBook-Pro.local>
Oliver Pomeroy <oppomeroy@gmail.com>
Ouyang Liduo <oyld0210@163.com>
Patrick Stapleton <github@gdi2290.com>
Paul Liljenberg <liljenberg.paul@gmail.com> <letters@paulnotcom.se>
Pavel Tikhomirov <ptikhomirov@virtuozzo.com> <ptikhomirov@parallels.com>
Pawel Konczalski <mail@konczalski.de>
Paweł Pokrywka <pepawel@users.noreply.github.com>
Per Lundberg <perlun@gmail.com>
Per Lundberg <perlun@gmail.com> <per.lundberg@ecraft.com>
Per Lundberg <perlun@gmail.com> <per.lundberg@hibox.tv>
Peter Choi <phkchoi89@gmail.com> <reikani@Peters-MacBook-Pro.local>
Peter Dave Hello <hsu@peterdavehello.org> <PeterDaveHello@users.noreply.github.com>
Peter Hsu <shhsu@microsoft.com>
Peter Jaffe <pjaffe@nevo.com>
Peter Nagy <xificurC@gmail.com> <pnagy@gratex.com>
Peter Waller <p@pwaller.net> <peter@scraperwiki.com>
Phil Estes <estesp@gmail.com>
Phil Estes <estesp@gmail.com> <estesp@amazon.com>
Phil Estes <estesp@gmail.com> <estesp@linux.vnet.ibm.com>
Phil Estes <estesp@linux.vnet.ibm.com> <estesp@gmail.com>
Philip Alexander Etling <paetling@gmail.com>
Philipp Gillé <philipp.gille@gmail.com> <philippgille@users.noreply.github.com>
Qiang Huang <h.huangqiang@huawei.com>
Qiang Huang <h.huangqiang@huawei.com> <qhuang@10.0.2.15>
Ray Tsang <rayt@google.com> <saturnism@users.noreply.github.com>
Renaud Gaubert <rgaubert@nvidia.com> <renaud.gaubert@gmail.com>
Rob Murray <rob.murray@docker.com>
Rob Murray <rob.murray@docker.com> <148866618+robmry@users.noreply.github.com>
Robert Terhaar <rterhaar@atlanticdynamic.com> <robbyt@users.noreply.github.com>
Roberto G. Hashioka <roberto.hashioka@docker.com> <roberto_hashioka@hotmail.com>
Roberto Muñoz Fernández <robertomf@gmail.com> <roberto.munoz.fernandez.contractor@bbva.com>
Roch Feuillade <roch.feuillade@pandobac.com>
Roch Feuillade <roch.feuillade@pandobac.com> <46478807+rochfeu@users.noreply.github.com>
Roman Dudin <katrmr@gmail.com> <decadent@users.noreply.github.com>
Ross Boucher <rboucher@gmail.com>
Rui JingAn <quiterace@gmail.com>
Runshen Zhu <runshen.zhu@gmail.com>
Ryan Stelly <ryan.stelly@live.com>
Sakeven Jiang <jc5930@sina.cn>
Samarth Shah <samashah@microsoft.com>
Sandeep Bansal <sabansal@microsoft.com>
Sandeep Bansal <sabansal@microsoft.com> <msabansal@microsoft.com>
Sandro Jäckel <sandro.jaeckel@gmail.com>
Sargun Dhillon <sargun@netflix.com> <sargun@sargun.me>
Saurabh Kumar <saurabhkumar0184@gmail.com>
Sean Lee <seanlee@tw.ibm.com> <scaleoutsean@users.noreply.github.com>
Sebastiaan van Stijn <github@gone.nl> <sebastiaan@ws-key-sebas3.dpi1.dpi>
Sebastiaan van Stijn <github@gone.nl> <thaJeztah@users.noreply.github.com>
@ -482,7 +387,6 @@ Solomon Hykes <solomon@docker.com> <solomon.hykes@dotcloud.com>
Solomon Hykes <solomon@docker.com> <solomon@dotcloud.com>
Soshi Katsuta <soshi.katsuta@gmail.com>
Soshi Katsuta <soshi.katsuta@gmail.com> <katsuta_soshi@cyberagent.co.jp>
Spring Lee <xi.shuai@outlook.com>
Sridhar Ratnakumar <sridharr@activestate.com>
Sridhar Ratnakumar <sridharr@activestate.com> <github@srid.name>
Srini Brahmaroutu <srbrahma@us.ibm.com> <sbrahma@us.ibm.com>
@ -512,11 +416,8 @@ Sven Dowideit <SvenDowideit@home.org.au> <SvenDowideit@users.noreply.github.com>
Sven Dowideit <SvenDowideit@home.org.au> <¨SvenDowideit@home.org.au¨>
Sylvain Bellemare <sylvain@ascribe.io>
Sylvain Bellemare <sylvain@ascribe.io> <sylvain.bellemare@ezeep.com>
Takeshi Koenuma <t.koenuma2@gmail.com>
Tangi Colin <tangicolin@gmail.com>
Teiva Harsanyi <t.harsanyi@thebeat.co>
Tejesh Mehta <tejesh.mehta@gmail.com> <tj@init.me>
Teppei Fukuda <knqyf263@gmail.com>
Thatcher Peskens <thatcher@docker.com>
Thatcher Peskens <thatcher@docker.com> <thatcher@dotcloud.com>
Thatcher Peskens <thatcher@docker.com> <thatcher@gmx.net>
@ -532,8 +433,6 @@ Tim Bart <tim@fewagainstmany.com>
Tim Bosse <taim@bosboot.org> <maztaim@users.noreply.github.com>
Tim Ruffles <oi@truffles.me.uk> <timruffles@googlemail.com>
Tim Terhorst <mynamewastaken+git@gmail.com>
Tim Welsh <timothy.welsh@docker.com>
Tim Welsh <timothy.welsh@docker.com> <84401379+twelsh-aw@users.noreply.github.com>
Tim Zju <21651152@zju.edu.cn>
Timothy Hobbs <timothyhobbs@seznam.cz>
Toli Kuznets <toli@docker.com>
@ -541,16 +440,10 @@ Tom Barlow <tomwbarlow@gmail.com>
Tom Milligan <code@tommilligan.net>
Tom Milligan <code@tommilligan.net> <tommilligan@users.noreply.github.com>
Tom Sweeney <tsweeney@redhat.com>
Tomas Bäckman <larstomas@gmail.com>
Tomas Bäckman <larstomas@gmail.com> <11527327+larstomas@users.noreply.github.com>
Tõnis Tiigi <tonistiigi@gmail.com>
Trishna Guha <trishnaguha17@gmail.com>
Tristan Carel <tristan@cogniteev.com>
Tristan Carel <tristan@cogniteev.com> <tristan.carel@gmail.com>
Ulrich Bareth <ulrich.bareth@gmail.com>
Ulrich Bareth <ulrich.bareth@gmail.com> <usb79@users.noreply.github.com>
Ulysses Souza <ulysses.souza@docker.com>
Ulysses Souza <ulysses.souza@docker.com> <ulyssessouza@gmail.com>
Umesh Yadav <umesh4257@gmail.com>
Umesh Yadav <umesh4257@gmail.com> <dungeonmaster18@users.noreply.github.com>
Victor Lyuboslavsky <victor@victoreda.com>
@ -609,4 +502,3 @@ Zhou Hao <zhouhao@cn.fujitsu.com>
Zhoulin Xie <zhoulin.xie@daocloud.io>
Zhu Kunjia <zhu.kunjia@zte.com.cn>
Zou Yu <zouyu7@huawei.com>
Александр Менщиков <__Singleton__@hackerdom.ru>

228
AUTHORS
View File

@ -1,74 +1,53 @@
# File @generated by scripts/docs/generate-authors.sh. DO NOT EDIT.
# This file lists all contributors to the repository.
# See scripts/docs/generate-authors.sh to make modifications.
# This file lists all individuals having contributed content to the repository.
# For how it is generated, see `scripts/docs/generate-authors.sh`.
A. Lester Buck III <github-reg@nbolt.com>
Aanand Prasad <aanand.prasad@gmail.com>
Aaron L. Xu <liker.xu@foxmail.com>
Aaron Lehmann <alehmann@netflix.com>
Aaron Lehmann <aaron.lehmann@docker.com>
Aaron.L.Xu <likexu@harmonycloud.cn>
Abdur Rehman <abdur_rehman@mentor.com>
Abhinandan Prativadi <abhi@docker.com>
Abin Shahab <ashahab@altiscale.com>
Abreto FU <public@abreto.email>
Ace Tang <aceapril@126.com>
Addam Hardy <addam.hardy@gmail.com>
Adolfo Ochagavía <aochagavia92@gmail.com>
Adrian Plata <adrian.plata@docker.com>
Adrien Duermael <adrien@duermael.com>
Adrien Folie <folie.adrien@gmail.com>
Adyanth Hosavalike <ahosavalike@ucsd.edu>
Ahmet Alp Balkan <ahmetb@microsoft.com>
Aidan Feldman <aidan.feldman@gmail.com>
Aidan Hobson Sayers <aidanhs@cantab.net>
AJ Bowen <aj@soulshake.net>
Akhil Mohan <akhil.mohan@mayadata.io>
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
AJ Bowen <aj@gandi.net>
Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
Akim Demaille <akim.demaille@docker.com>
Alan Thompson <cloojure@gmail.com>
Alano Terblanche <alano.terblanche@docker.com>
Albert Callarisa <shark234@gmail.com>
Alberto Roura <mail@albertoroura.com>
Albin Kerouanton <albinker@gmail.com>
Aleksa Sarai <asarai@suse.de>
Aleksander Piotrowski <apiotrowski312@gmail.com>
Alessandro Boch <aboch@tetrationanalytics.com>
Alex Couture-Beil <alex@earthly.dev>
Alex Mavrogiannis <alex.mavrogiannis@docker.com>
Alex Mayer <amayer5125@gmail.com>
Alexander Boyd <alex@opengroove.org>
Alexander Chneerov <achneerov@gmail.com>
Alexander Larsson <alexl@redhat.com>
Alexander Morozov <lk4d4math@gmail.com>
Alexander Morozov <lk4d4@docker.com>
Alexander Ryabov <i@sepa.spb.ru>
Alexandre González <agonzalezro@gmail.com>
Alexey Igrychev <alexey.igrychev@flant.com>
Alexis Couvreur <alexiscouvreur.pro@gmail.com>
Alfred Landrum <alfred.landrum@docker.com>
Ali Rostami <rostami.ali@gmail.com>
Alicia Lauerman <alicia@eta.im>
Allen Sun <allensun.shl@alibaba-inc.com>
Alvin Deng <alvin.q.deng@utexas.edu>
Amen Belayneh <amenbelayneh@gmail.com>
Amey Shrivastava <72866602+AmeyShrivastava@users.noreply.github.com>
Amir Goldstein <amir73il@aquasec.com>
Amit Krishnan <amit.krishnan@oracle.com>
Amit Shukla <amit.shukla@docker.com>
Amy Lindburg <amy.lindburg@docker.com>
Anca Iordache <anca.iordache@docker.com>
Anda Xu <anda.xu@docker.com>
Andrea Luzzardi <aluzzardi@gmail.com>
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 Hsu <andrewhsu@docker.com>
Andrew Macpherson <hopscotch23@gmail.com>
Andrew McDonnell <bugs@andrewmcdonnell.net>
Andrew Po <absourd.noise@gmail.com>
Andrew-Zipperer <atzipperer@gmail.com>
Andrey Petrov <andrey.petrov@shazow.net>
Andrii Berehuliak <berkusandrew@gmail.com>
André Martins <aanm90@gmail.com>
Andy Goldstein <agoldste@redhat.com>
Andy Rothfusz <github@developersupport.net>
@ -81,37 +60,25 @@ Antonis Kalipetis <akalipetis@gmail.com>
Anusha Ragunathan <anusha.ragunathan@docker.com>
Ao Li <la9249@163.com>
Arash Deshmeh <adeshmeh@ca.ibm.com>
Arko Dasgupta <arko@tetrate.io>
Arnaud Porterie <icecrime@gmail.com>
Arnaud Rebillout <elboulangero@gmail.com>
Arthur Peka <arthur.peka@outlook.com>
Ashly Mathew <ashly.mathew@sap.com>
Arnaud Porterie <arnaud.porterie@docker.com>
Ashwini Oruganti <ashwini.oruganti@gmail.com>
Aslam Ahemad <aslamahemad@gmail.com>
Azat Khuyiyakhmetov <shadow_uz@mail.ru>
Bardia Keyoumarsi <bkeyouma@ucsc.edu>
Barnaby Gray <barnaby@pickle.me.uk>
Bastiaan Bakker <bbakker@xebia.com>
BastianHofmann <bastianhofmann@me.com>
Ben Bodenmiller <bbodenmiller@gmail.com>
Ben Bonnefoy <frenchben@docker.com>
Ben Creasy <ben@bencreasy.com>
Ben Firshman <ben@firshman.co.uk>
Benjamin Boudreau <boudreau.benjamin@gmail.com>
Benjamin Böhmke <benjamin@boehmke.net>
Benjamin Nater <me@bn4t.me>
Benoit Sigoure <tsunanet@gmail.com>
Bhumika Bayani <bhumikabayani@gmail.com>
Bill Wang <ozbillwang@gmail.com>
Bin Liu <liubin0329@gmail.com>
Bingshen Wang <bingshen.wbs@alibaba-inc.com>
Bishal Das <bishalhnj127@gmail.com>
Bjorn Neergaard <bjorn.neergaard@docker.com>
Boaz Shuster <ripcurld.github@gmail.com>
Boban Acimovic <boban.acimovic@gmail.com>
Bogdan Anton <contact@bogdananton.ro>
Boris Pruessmann <boris@pruessmann.org>
Brad Baker <brad@brad.fi>
Bradley Cicenas <bradley.cicenas@gmail.com>
Brandon Mitchell <git@bmitch.net>
Brandon Philips <brandon.philips@coreos.com>
@ -119,20 +86,14 @@ Brent Salisbury <brent.salisbury@docker.com>
Bret Fisher <bret@bretfisher.com>
Brian (bex) Exelbierd <bexelbie@redhat.com>
Brian Goff <cpuguy83@gmail.com>
Brian Tracy <brian.tracy33@gmail.com>
Brian Wieder <brian@4wieders.com>
Bruno Sousa <bruno.sousa@docker.com>
Bryan Bess <squarejaw@bsbess.com>
Bryan Boreham <bjboreham@gmail.com>
Bryan Murphy <bmurphy1976@gmail.com>
bryfry <bryon.fryer@gmail.com>
Calvin Liu <flycalvin@qq.com>
Cameron Spear <cameronspear@gmail.com>
Cao Weiwei <cao.weiwei30@zte.com.cn>
Carlo Mion <mion00@gmail.com>
Carlos Alexandro Becker <caarlos0@gmail.com>
Carlos de Paula <me@carlosedp.com>
Casey Korver <casey@korver.dev>
Ce Gao <ce.gao@outlook.com>
Cedric Davies <cedricda@microsoft.com>
Cezar Sa Espinola <cezarsa@gmail.com>
@ -142,20 +103,15 @@ Charles Chan <charleswhchan@users.noreply.github.com>
Charles Law <claw@conduce.com>
Charles Smith <charles.smith@docker.com>
Charlie Drage <charlie@charliedrage.com>
Charlotte Mach <charlotte.mach@fs.lmu.de>
ChaYoung You <yousbe@gmail.com>
Chee Hau Lim <cheehau.lim@mobimeo.com>
Chen Chuanliang <chen.chuanliang@zte.com.cn>
Chen Hanxiao <chenhanxiao@cn.fujitsu.com>
Chen Mingjie <chenmingjie0828@163.com>
Chen Qiu <cheney-90@hotmail.com>
Chris Chinchilla <chris@chrischinchilla.com>
Chris Couzens <ccouzens@gmail.com>
Chris Gavin <chris@chrisgavin.me>
Chris Gibson <chris@chrisg.io>
Chris McKinnel <chrismckinnel@gmail.com>
Chris Snow <chsnow123@gmail.com>
Chris Vermilion <christopher.vermilion@gmail.com>
Chris Weyl <cweyl@alumni.drew.edu>
Christian Persson <saser@live.se>
Christian Stefanescu <st.chris@gmail.com>
@ -164,9 +120,6 @@ Christophe Vidal <kriss@krizalys.com>
Christopher Biscardi <biscarch@sketcht.com>
Christopher Crone <christopher.crone@docker.com>
Christopher Jones <tophj@linux.vnet.ibm.com>
Christopher Petito <47751006+krissetto@users.noreply.github.com>
Christopher Petito <chrisjpetito@gmail.com>
Christopher Svensson <stoffus@stoffus.com>
Christy Norman <christy@linux.vnet.ibm.com>
Chun Chen <ramichen@tencent.com>
Clinton Kitson <clintonskitson@gmail.com>
@ -174,36 +127,25 @@ Coenraad Loubser <coenraad@wish.org.za>
Colin Hebert <hebert.colin@gmail.com>
Collin Guarino <collin.guarino@gmail.com>
Colm Hally <colmhally@gmail.com>
Comical Derskeal <27731088+derskeal@users.noreply.github.com>
Conner Crosby <conner@cavcrosby.tech>
Corey Farrell <git@cfware.com>
Corey Quon <corey.quon@docker.com>
Cory Bennet <cbennett@netflix.com>
Cory Snider <csnider@mirantis.com>
Craig Osterhout <craig.osterhout@docker.com>
Craig Wilhite <crwilhit@microsoft.com>
Cristian Staretu <cristian.staretu@gmail.com>
Daehyeok Mun <daehyeok@gmail.com>
Dafydd Crosby <dtcrsby@gmail.com>
Daisuke Ito <itodaisuke00@gmail.com>
dalanlan <dalanlan925@gmail.com>
Damien Nadé <github@livna.org>
Dan Cotora <dan@bluevision.ro>
Danial Gharib <danial.mail.gh@gmail.com>
Daniel Artine <daniel.artine@ufrj.br>
Daniel Cassidy <mail@danielcassidy.me.uk>
Daniel Dao <dqminh@cloudflare.com>
Daniel Farrell <dfarrell@redhat.com>
Daniel Gasienica <daniel@gasienica.ch>
Daniel Goosen <daniel.goosen@surveysampling.com>
Daniel Helfand <dhelfand@redhat.com>
Daniel Hiltgen <daniel.hiltgen@docker.com>
Daniel J Walsh <dwalsh@redhat.com>
Daniel Nephin <dnephin@docker.com>
Daniel Norberg <dano@spotify.com>
Daniel Watkins <daniel@daniel-watkins.co.uk>
Daniel Zhang <jmzwcn@gmail.com>
Daniil Nikolenko <qoo2p5@gmail.com>
Danny Berger <dpb587@gmail.com>
Darren Shepherd <darren.s.shepherd@gmail.com>
Darren Stahl <darst@microsoft.com>
@ -211,14 +153,11 @@ Dattatraya Kumbhar <dattatraya.kumbhar@gslab.com>
Dave Goodchild <buddhamagnet@gmail.com>
Dave Henderson <dhenderson@gmail.com>
Dave Tucker <dt@docker.com>
David Alvarez <david.alvarez@flyeralarm.com>
David Beitey <david@davidjb.com>
David Calavera <david.calavera@gmail.com>
David Cramer <davcrame@cisco.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>
David Lechner <david@lechnology.com>
David Scott <dave@recoil.org>
David Sheets <dsheets@docker.com>
@ -230,9 +169,7 @@ Denis Defreyne <denis@soundcloud.com>
Denis Gladkikh <denis@gladkikh.email>
Denis Ollier <larchunix@users.noreply.github.com>
Dennis Docter <dennis@d23.nl>
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Derek McGowan <derek@mcg.dev>
Des Preston <despreston@gmail.com>
Derek McGowan <derek@mcgstyle.net>
Deshi Xiao <dxiao@redhat.com>
Dharmit Shah <shahdharmit@gmail.com>
Dhawal Yogesh Bhanushali <dbhanushali@vmware.com>
@ -241,34 +178,26 @@ Dima Stopel <dima@twistlock.com>
Dimitry Andric <d.andric@activevideo.com>
Ding Fei <dingfei@stars.org.cn>
Diogo Monica <diogo@docker.com>
Djordje Lukic <djordje.lukic@docker.com>
Dmitriy Fishman <fishman.code@gmail.com>
Dmitry Gusev <dmitry.gusev@gmail.com>
Dmitry Smirnov <onlyjob@member.fsf.org>
Dmitry V. Krivenok <krivenok.dmitry@gmail.com>
Dominik Braun <dominik.braun@nbsp.de>
Don Kjer <don.kjer@gmail.com>
Dong Chen <dongluo.chen@docker.com>
DongGeon Lee <secmatth1996@gmail.com>
Doug Davis <dug@us.ibm.com>
Drew Erny <derny@mirantis.com>
Drew Erny <drew.erny@docker.com>
Ed Costello <epc@epcostello.com>
Ed Morley <501702+edmorley@users.noreply.github.com>
Elango Sivanandam <elango.siva@docker.com>
Eli Uriegas <eli.uriegas@docker.com>
Eli Uriegas <seemethere101@gmail.com>
Elias Faxö <elias.faxo@tre.se>
Elliot Luo <956941328@qq.com>
Eric Bode <eric.bode@foundries.io>
Eric Curtin <ericcurtin17@gmail.com>
Eric Engestrom <eric@engestrom.ch>
Eric G. Noriega <enoriega@vizuri.com>
Eric Rosenberg <ehaydenr@gmail.com>
Eric Sage <eric.david.sage@gmail.com>
Eric-Olivier Lamey <eo@lamey.me>
Erica Windisch <erica@windisch.us>
Erik Hollensbe <github@hollensbe.org>
Erik Humphrey <erik.humphrey@carleton.ca>
Erik St. Martin <alakriti@gmail.com>
Essam A. Hassan <es.hassan187@gmail.com>
Ethan Haynes <ethanhaynes@alumni.harvard.edu>
@ -277,19 +206,15 @@ Eugene Yakubovich <eugene.yakubovich@coreos.com>
Evan Allrich <evan@unguku.com>
Evan Hazlett <ejhazlett@gmail.com>
Evan Krall <krall@yelp.com>
Evan Lezar <elezar@nvidia.com>
Evelyn Xu <evelynhsu21@gmail.com>
Everett Toews <everett.toews@rackspace.com>
Fabio Falci <fabiofalci@gmail.com>
Fabrizio Soppelsa <fsoppelsa@mirantis.com>
Felix Geyer <debfx@fobos.de>
Felix Hupfeld <felix@quobyte.com>
Felix Rabe <felix@rabe.io>
fezzik1620 <fezzik1620@users.noreply.github.com>
Filip Jareš <filipjares@gmail.com>
Flavio Crisciani <flavio.crisciani@docker.com>
Florian Klein <florian.klein@free.fr>
Forest Johnson <fjohnson@peoplenetonline.com>
Foysal Iqbal <foysal.iqbal.fb@gmail.com>
François Scala <francois.scala@swiss-as.com>
Fred Lifton <fred.lifton@docker.com>
@ -297,32 +222,21 @@ Frederic Hemberger <mail@frederic-hemberger.de>
Frederick F. Kautz IV <fkautz@redhat.com>
Frederik Nordahl Jul Sabroe <frederikns@gmail.com>
Frieder Bluemle <frieder.bluemle@gmail.com>
Gabriel Gore <gabgore@cisco.com>
Gabriel Nicolas Avellaneda <avellaneda.gabriel@gmail.com>
Gabriela Georgieva <gabriela.georgieva@docker.com>
Gaetan de Villele <gdevillele@gmail.com>
Gang Qiao <qiaohai8866@gmail.com>
Gary Schaetz <gary@schaetzkc.com>
Genki Takiuchi <genki@s21g.com>
George MacRorie <gmacr31@gmail.com>
George Margaritis <gmargaritis@protonmail.com>
George Xie <georgexsh@gmail.com>
Gianluca Borello <g.borello@gmail.com>
Gildas Cuisinier <gildas.cuisinier@gcuisinier.net>
Gio d'Amelio <giodamelio@gmail.com>
Gleb Stsenov <gleb.stsenov@gmail.com>
Goksu Toprak <goksu.toprak@docker.com>
Gou Rao <gou@portworx.com>
Govind Rai <raigovind93@gmail.com>
Grace Choi <grace.54109@gmail.com>
Graeme Wiebe <graeme.wiebe@gmail.com>
Grant Reaber <grant.reaber@gmail.com>
Greg Pflaum <gpflaum@users.noreply.github.com>
Gsealy <jiaojingwei1001@hotmail.com>
Guilhem Lettron <guilhem+github@lettron.fr>
Guillaume J. Charmes <guillaume.charmes@docker.com>
Guillaume Le Floch <glfloch@gmail.com>
Guillaume Tardif <guillaume.tardif@gmail.com>
gwx296173 <gaojing3@huawei.com>
Günther Jungbluth <gunther@gameslabs.net>
Hakan Özler <hakan.ozler@kodcu.com>
@ -331,7 +245,6 @@ Harald Albers <github@albersweb.de>
Harold Cooper <hrldcpr@gmail.com>
Harry Zhang <harryz@hyper.sh>
He Simei <hesimei@zju.edu.cn>
Hector S <hfsam88@gmail.com>
Helen Xie <chenjg@harmonycloud.cn>
Henning Sprang <henning.sprang@gmail.com>
Henry N <henrynmail-github@yahoo.de>
@ -339,12 +252,9 @@ Hernan Garcia <hernandanielg@gmail.com>
Hongbin Lu <hongbin034@gmail.com>
Hu Keping <hukeping@huawei.com>
Huayi Zhang <irachex@gmail.com>
Hugo Chastel <Hugo-C@users.noreply.github.com>
Hugo Gabriel Eyherabide <hugogabriel.eyherabide@gmail.com>
huqun <huqun@zju.edu.cn>
Huu Nguyen <huu@prismskylabs.com>
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com>
Iain Samuel McLean Elder <iain@isme.es>
Ian Campbell <ian.campbell@docker.com>
Ian Philpot <ian.philpot@microsoft.com>
Ignacio Capurro <icapurrofagian@gmail.com>
@ -354,16 +264,12 @@ Ilya Sotkov <ilya@sotkov.com>
Ioan Eugen Stan <eu@ieugen.ro>
Isabel Jimenez <contact.isabeljimenez@gmail.com>
Ivan Grcic <igrcic@gmail.com>
Ivan Grund <ivan.grund@gmail.com>
Ivan Markin <sw@nogoegst.net>
Jacob Atzen <jacob@jacobatzen.dk>
Jacob Tomlinson <jacob@tom.linson.uk>
Jacopo Rigoli <rigoli.jacopo@gmail.com>
Jaivish Kothari <janonymous.codevulture@gmail.com>
Jake Lambert <jake.lambert@volusion.com>
Jake Sanders <jsand@google.com>
Jake Stokes <contactjake@developerjake.com>
Jakub Panek <me@panekj.dev>
James Nesbitt <james.nesbitt@wunderkraut.com>
James Turnbull <james@lovedthanlost.net>
Jamie Hannaford <jamie@limetree.org>
@ -373,37 +279,31 @@ Jan-Jaap Driessen <janjaapdriessen@gmail.com>
Jana Radhakrishnan <mrjana@docker.com>
Jared Hocutt <jaredh@netapp.com>
Jasmine Hegman <jasmine@jhegman.com>
Jason Hall <jason@chainguard.dev>
Jason Heiss <jheiss@aput.net>
Jason Plum <jplum@devonit.com>
Jay Kamat <github@jgkamat.33mail.com>
Jean Lecordier <jeanlecordier@hotmail.fr>
Jean Rouge <rougej+github@gmail.com>
Jean-Christophe Sirot <jean-christophe.sirot@docker.com>
Jean-Pierre Huynh <jean-pierre.huynh@ounet.fr>
Jeff Lindsay <progrium@gmail.com>
Jeff Nickoloff <jeff.nickoloff@gmail.com>
Jeff Silberman <jsilberm@gmail.com>
Jennings Zhang <jenni_zh@protonmail.com>
Jeremy Chambers <jeremy@thehipbot.com>
Jeremy Unruh <jeremybunruh@gmail.com>
Jeremy Yallop <yallop@docker.com>
Jeroen Franse <jeroenfranse@gmail.com>
Jesse Adametz <jesseadametz@gmail.com>
Jessica Frazelle <jess@oxide.computer>
Jessica Frazelle <jessfraz@google.com>
Jezeniel Zapanta <jpzapanta22@gmail.com>
Jian Zhang <zhangjian.fnst@cn.fujitsu.com>
Jie Luo <luo612@zju.edu.cn>
Jilles Oldenbeuving <ojilles@gmail.com>
Jim Chen <njucjc@gmail.com>
Jim Galasyn <jim.galasyn@docker.com>
Jim Lin <b04705003@ntu.edu.tw>
Jimmy Leger <jimmy.leger@gmail.com>
Jimmy Song <rootsongjc@gmail.com>
jimmyxian <jimmyxian2004@yahoo.com.cn>
Jintao Zhang <zhangjintao9020@gmail.com>
Joao Fernandes <joao.fernandes@docker.com>
Joe Abbey <joe.abbey@gmail.com>
Joe Doliner <jdoliner@pachyderm.io>
Joe Gordon <joe.gordon0@gmail.com>
Joel Handwell <joelhandwell@gmail.com>
@ -413,8 +313,7 @@ Johan Euphrosine <proppy@google.com>
Johannes 'fish' Ziemke <github@freigeist.org>
John Feminella <jxf@jxf.me>
John Harris <john@johnharris.io>
John Howard <github@lowenna.com>
John Howard <howardjohn@google.com>
John Howard (VM) <John.Howard@microsoft.com>
John Laswell <john.n.laswell@gmail.com>
John Maguire <jmaguire@duosecurity.com>
John Mulhausen <john@docker.com>
@ -423,31 +322,22 @@ John Stephens <johnstep@docker.com>
John Tims <john.k.tims@gmail.com>
John V. Martinez <jvmatl@gmail.com>
John Willis <john.willis@docker.com>
Jon Johnson <jonjohnson@google.com>
Jon Zeolla <zeolla@gmail.com>
Jonatas Baldin <jonatas.baldin@gmail.com>
Jonathan A. Sternberg <jonathansternberg@gmail.com>
Jonathan Boulle <jonathanboulle@gmail.com>
Jonathan Lee <jonjohn1232009@gmail.com>
Jonathan Lomas <jonathan@floatinglomas.ca>
Jonathan McCrohan <jmccrohan@gmail.com>
Jonathan Warriss-Simmons <misterws@diogenes.ws>
Jonh Wendell <jonh.wendell@redhat.com>
Jordan Jennings <jjn2009@gmail.com>
Jorge Vallecillo <jorgevallecilloc@gmail.com>
Jose J. Escobar <53836904+jescobar-docker@users.noreply.github.com>
Joseph Kern <jkern@semafour.net>
Josh Bodah <jb3689@yahoo.com>
Josh Chorlton <jchorlton@gmail.com>
Josh Hawn <josh.hawn@docker.com>
Josh Horwitz <horwitz@addthis.com>
Josh Soref <jsoref@gmail.com>
Julian <gitea+julian@ic.thejulian.uk>
Julien Barbier <write0@gmail.com>
Julien Kassar <github@kassisol.com>
Julien Maitrehenry <julien.maitrehenry@me.com>
Justas Brazauskas <brazauskasjustas@gmail.com>
Justin Chadwell <me@jedevc.com>
Justin Cormack <justin.cormack@docker.com>
Justin Simonelis <justin.p.simonelis@gmail.com>
Justyn Temme <justyntemme@gmail.com>
@ -461,49 +351,42 @@ Kara Alexandra <kalexandra@us.ibm.com>
Kareem Khazem <karkhaz@karkhaz.com>
Karthik Nayak <Karthik.188@gmail.com>
Kat Samperi <kat.samperi@gmail.com>
Kathryn Spiers <kathryn@spiers.me>
Katie McLaughlin <katie@glasnt.com>
Ke Xu <leonhartx.k@gmail.com>
Kei Ohmura <ohmura.kei@gmail.com>
Keith Hudgins <greenman@greenman.org>
Kelton Bassingthwaite <KeltonBassingthwaite@gmail.com>
Ken Cochrane <kencochrane@gmail.com>
Ken ICHIKAWA <ichikawa.ken@jp.fujitsu.com>
Kenfe-Mickaël Laventure <mickael.laventure@gmail.com>
Kevin Alvarez <github@crazymax.dev>
Kevin Burke <kev@inburke.com>
Kevin Feyrer <kevin.feyrer@btinternet.com>
Kevin Kern <kaiwentan@harmonycloud.cn>
Kevin Kirsche <Kev.Kirsche+GitHub@gmail.com>
Kevin Meredith <kevin.m.meredith@gmail.com>
Kevin Richardson <kevin@kevinrichardson.co>
Kevin Woblick <mail@kovah.de>
khaled souf <khaled.souf@gmail.com>
Kim Eik <kim@heldig.org>
Kir Kolyshkin <kolyshkin@gmail.com>
Kirill A. Korinsky <kirill@korins.ky>
Kotaro Yoshimatsu <kotaro.yoshimatsu@gmail.com>
Krasi Georgiev <krasi@vip-consult.solutions>
Kris-Mikael Krister <krismikael@protonmail.com>
Kun Zhang <zkazure@gmail.com>
Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp>
Kyle Mitofsky <Kylemit@gmail.com>
Kyle Spiers <kyle@spiers.me>
Lachlan Cooper <lachlancooper@gmail.com>
Lai Jiangshan <jiangshanlai@gmail.com>
Lars Kellogg-Stedman <lars@redhat.com>
Laura Brehm <laurabrehm@hey.com>
Laura Frank <ljfrank@gmail.com>
Laurent Erignoux <lerignoux@gmail.com>
Lee Gaines <eightlimbed@gmail.com>
Lei Jitang <leijitang@huawei.com>
Lennie <github@consolejunkie.net>
Leo Gallucci <elgalu3@gmail.com>
Leonid Skorospelov <leosko94@gmail.com>
Lewis Daly <lewisdaly@me.com>
Li Fu Bang <lifubang@acmcoder.com>
Li Yi <denverdino@gmail.com>
Li Yi <weiyuan.yl@alibaba-inc.com>
Liang-Chi Hsieh <viirya@gmail.com>
Lifubang <lifubang@acmcoder.com>
Lihua Tang <lhtang@alauda.io>
Lily Guo <lily.guo@docker.com>
Lin Lu <doraalin@163.com>
@ -518,31 +401,23 @@ Louis Opter <kalessin@kalessin.fr>
Luca Favatella <luca.favatella@erlang-solutions.com>
Luca Marturana <lucamarturana@gmail.com>
Lucas Chan <lucas-github@lucaschan.com>
Luis Henrique Mulinari <luis.mulinari@gmail.com>
Luka Hartwig <mail@lukahartwig.de>
Lukas Heeren <lukas-heeren@hotmail.com>
Lukasz Zajaczkowski <Lukasz.Zajaczkowski@ts.fujitsu.com>
Lydell Manganti <LydellManganti@users.noreply.github.com>
Lénaïc Huard <lhuard@amadeus.com>
Ma Shimiao <mashimiao.fnst@cn.fujitsu.com>
Mabin <bin.ma@huawei.com>
Maciej Kalisz <maciej.d.kalisz@gmail.com>
Madhav Puri <madhav.puri@gmail.com>
Madhu Venugopal <madhu@socketplane.io>
Madhur Batra <madhurbatra097@gmail.com>
Malte Janduda <mail@janduda.net>
Manjunath A Kumatagi <mkumatag@in.ibm.com>
Mansi Nahar <mmn4185@rit.edu>
mapk0y <mapk0y@gmail.com>
Marc Bihlmaier <marc.bihlmaier@reddoxx.com>
Marc Cornellà <hello@mcornella.com>
Marco Mariani <marco.mariani@alterway.fr>
Marco Spiess <marco.spiess@hotmail.de>
Marco Vedovati <mvedovati@suse.com>
Marcus Martins <marcus@docker.com>
Marianna Tessel <mtesselh@gmail.com>
Marius Ileana <marius.ileana@gmail.com>
Marius Meschter <marius@meschter.me>
Marius Sturm <marius@graylog.com>
Mark Oates <fl0yd@me.com>
Marsh Macy <marsma@microsoft.com>
@ -551,9 +426,7 @@ Mary Anthony <mary.anthony@docker.com>
Mason Fish <mason.fish@docker.com>
Mason Malone <mason.malone@gmail.com>
Mateusz Major <apkd@users.noreply.github.com>
Mathias Duedahl <64321057+Lussebullen@users.noreply.github.com>
Mathieu Champlon <mathieu.champlon@docker.com>
Mathieu Rollet <matletix@gmail.com>
Matt Gucci <matt9ucci@gmail.com>
Matt Robenolt <matt@ydekproductions.com>
Matteo Orefice <matteo.orefice@bites4bits.software>
@ -561,16 +434,12 @@ Matthew Heon <mheon@redhat.com>
Matthieu Hauglustaine <matt.hauglustaine@gmail.com>
Mauro Porras P <mauroporrasp@gmail.com>
Max Shytikov <mshytikov@gmail.com>
Max-Julian Pogner <max-julian@pogner.at>
Maxime Petazzoni <max@signalfuse.com>
Maximillian Fan Xavier <maximillianfx@gmail.com>
Mei ChunTao <mei.chuntao@zte.com.cn>
Melroy van den Berg <melroy@melroy.org>
Metal <2466052+tedhexaflow@users.noreply.github.com>
Micah Zoltu <micah@newrelic.com>
Michael A. Smith <michael@smith-li.com>
Michael Bridgen <mikeb@squaremobius.net>
Michael Crosby <crosbymichael@gmail.com>
Michael Crosby <michael@docker.com>
Michael Friis <friism@gmail.com>
Michael Irwin <mikesir87@gmail.com>
Michael Käufl <docker@c.michael-kaeufl.de>
@ -586,7 +455,6 @@ Mihai Borobocea <MihaiBorob@gmail.com>
Mihuleacc Sergiu <mihuleac.sergiu@gmail.com>
Mike Brown <brownwm@us.ibm.com>
Mike Casas <mkcsas0@gmail.com>
Mike Dalton <mikedalton@github.com>
Mike Danese <mikedanese@google.com>
Mike Dillon <mike@embody.org>
Mike Goelzer <mike.goelzer@docker.com>
@ -595,20 +463,15 @@ mikelinjie <294893458@qq.com>
Mikhail Vasin <vasin@cloud-tv.ru>
Milind Chawre <milindchawre@gmail.com>
Mindaugas Rukas <momomg@gmail.com>
Miroslav Gula <miroslav.gula@naytrolabs.com>
Misty Stanley-Jones <misty@docker.com>
Mohammad Banikazemi <mb@us.ibm.com>
Mohammed Aaqib Ansari <maaquib@gmail.com>
Mohini Anne Dsouza <mohini3917@gmail.com>
Moorthy RS <rsmoorthy@gmail.com>
Morgan Bauer <mbauer@us.ibm.com>
Morten Hekkvang <morten.hekkvang@sbab.se>
Morten Linderud <morten@linderud.pw>
Moysés Borges <moysesb@gmail.com>
Mozi <29089388+pzhlkj6612@users.noreply.github.com>
Mrunal Patel <mrunalp@gmail.com>
muicoder <muicoder@gmail.com>
Murukesh Mohanan <murukesh.mohanan@gmail.com>
Muthukumar R <muthur@gmail.com>
Máximo Cuadros <mcuadros@gmail.com>
Mårten Cassel <marten.cassel@gmail.com>
@ -624,8 +487,6 @@ Nathan LeClaire <nathan.leclaire@docker.com>
Nathan McCauley <nathan.mccauley@docker.com>
Neil Peterson <neilpeterson@outlook.com>
Nick Adcock <nick.adcock@docker.com>
Nick Santos <nick.santos@docker.com>
Nick Sieger <nick@nicksieger.com>
Nico Stapelbroek <nstapelbroek@gmail.com>
Nicola Kabar <nicolaka@gmail.com>
Nicolas Borboën <ponsfrilus@gmail.com>
@ -638,34 +499,23 @@ Nishant Totla <nishanttotla@gmail.com>
NIWA Hideyuki <niwa.niwa@nifty.ne.jp>
Noah Treuhaft <noah.treuhaft@docker.com>
O.S. Tezer <ostezer@gmail.com>
Oded Arbel <oded@geek.co.il>
Odin Ugedal <odin@ugedal.com>
ohmystack <jun.jiang02@ele.me>
OKA Naoya <git@okanaoya.com>
Oliver Pomeroy <oppomeroy@gmail.com>
Olle Jonsson <olle.jonsson@gmail.com>
Olli Janatuinen <olli.janatuinen@gmail.com>
Oscar Wieman <oscrx@icloud.com>
Otto Kekäläinen <otto@seravo.fi>
Ovidio Mallo <ovidio.mallo@gmail.com>
Pascal Borreli <pascal@borreli.com>
Patrick Böänziger <patrick.baenziger@bsi-software.com>
Patrick Daigle <114765035+pdaig@users.noreply.github.com>
Patrick Hemmer <patrick.hemmer@gmail.com>
Patrick Lang <plang@microsoft.com>
Paul <paul9869@gmail.com>
Paul Kehrer <paul.l.kehrer@gmail.com>
Paul Lietar <paul@lietar.net>
Paul Mulders <justinkb@gmail.com>
Paul Seyfert <pseyfert.mathphys@gmail.com>
Paul Weaver <pauweave@cisco.com>
Pavel Pospisil <pospispa@gmail.com>
Paweł Gronowski <pawel.gronowski@docker.com>
Paweł Pokrywka <pepawel@users.noreply.github.com>
Paweł Szczekutowicz <pszczekutowicz@gmail.com>
Peeyush Gupta <gpeeyush@linux.vnet.ibm.com>
Per Lundberg <perlun@gmail.com>
Peter Dave Hello <hsu@peterdavehello.org>
Per Lundberg <per.lundberg@ecraft.com>
Peter Edge <peter.edge@gmail.com>
Peter Hsu <shhsu@microsoft.com>
Peter Jaffe <pjaffe@nevo.com>
@ -673,13 +523,11 @@ Peter Kehl <peter.kehl@gmail.com>
Peter Nagy <xificurC@gmail.com>
Peter Salvatore <peter@psftw.com>
Peter Waller <p@pwaller.net>
Phil Estes <estesp@gmail.com>
Phil Estes <estesp@linux.vnet.ibm.com>
Philip Alexander Etling <paetling@gmail.com>
Philipp Gillé <philipp.gille@gmail.com>
Philipp Schmied <pschmied@schutzwerk.com>
Phong Tran <tran.pho@northeastern.edu>
pidster <pid@pidster.com>
Pieter E Smit <diepes@github.com>
pixelistik <pixelistik@users.noreply.github.com>
Pratik Karki <prertik@outlook.com>
Prayag Verma <prayag.verma@gmail.com>
@ -687,40 +535,30 @@ Preston Cowley <preston.cowley@sony.com>
Pure White <daniel48@126.com>
Qiang Huang <h.huangqiang@huawei.com>
Qinglan Peng <qinglanpeng@zju.edu.cn>
QQ喵 <gqqnb2005@gmail.com>
qudongfang <qudongfang@gmail.com>
Raghavendra K T <raghavendra.kt@linux.vnet.ibm.com>
Rahul Kadyan <hi@znck.me>
Rahul Zoldyck <rahulzoldyck@gmail.com>
Ravi Shekhar Jethani <rsjethani@gmail.com>
Ray Tsang <rayt@google.com>
Reficul <xuzhenglun@gmail.com>
Remy Suen <remy.suen@gmail.com>
Renaud Gaubert <rgaubert@nvidia.com>
Ricardo N Feliciano <FelicianoTech@gmail.com>
Rich Moyse <rich@moyse.us>
Richard Chen Zheng <58443436+rchenzheng@users.noreply.github.com>
Richard Mathie <richard.mathie@amey.co.uk>
Richard Scothern <richard.scothern@gmail.com>
Rick Wieman <git@rickw.nl>
Ritesh H Shukla <sritesh@vmware.com>
Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
Rob Gulewich <rgulewich@netflix.com>
Rob Murray <rob.murray@docker.com>
Robert Wallis <smilingrob@gmail.com>
Robin Naundorf <r.naundorf@fh-muenster.de>
Robin Speekenbrink <robin@kingsquare.nl>
Roch Feuillade <roch.feuillade@pandobac.com>
Rodolfo Ortiz <rodolfo.ortiz@definityfirst.com>
Rogelio Canedo <rcanedo@mappy.priv>
Rohan Verma <hello@rohanverma.net>
Roland Kammerer <roland.kammerer@linbit.com>
Roman Dudin <katrmr@gmail.com>
Rory Hunter <roryhunter2@gmail.com>
Ross Boucher <rboucher@gmail.com>
Rubens Figueiredo <r.figueiredo.52@gmail.com>
Rui Cao <ruicao@alauda.io>
Rui JingAn <quiterace@gmail.com>
Ryan Belgrave <rmb1993@gmail.com>
Ryan Detzel <ryan.detzel@gmail.com>
Ryan Stelly <ryan.stelly@live.com>
@ -730,17 +568,10 @@ Sainath Grandhi <sainath.grandhi@intel.com>
Sakeven Jiang <jc5930@sina.cn>
Sally O'Malley <somalley@redhat.com>
Sam Neirinck <sam@samneirinck.com>
Sam Thibault <sam.thibault@docker.com>
Samarth Shah <samashah@microsoft.com>
Sambuddha Basu <sambuddhabasu1@gmail.com>
Sami Tabet <salph.tabet@gmail.com>
Samuel Cochran <sj26@sj26.com>
Samuel Karp <skarp@amazon.com>
Sandro Jäckel <sandro.jaeckel@gmail.com>
Santhosh Manohar <santhosh@docker.com>
Sargun Dhillon <sargun@netflix.com>
Saswat Bhattacharya <sas.saswat@gmail.com>
Saurabh Kumar <saurabhkumar0184@gmail.com>
Scott Brenner <scott@scottbrenner.me>
Scott Collier <emailscottcollier@gmail.com>
Sean Christopherson <sean.j.christopherson@intel.com>
@ -761,14 +592,12 @@ sidharthamani <sid@rancher.com>
Silvin Lubecki <silvin.lubecki@docker.com>
Simei He <hesimei@zju.edu.cn>
Simon Ferquel <simon.ferquel@docker.com>
Simon Heimberg <simon.heimberg@heimberg-ea.ch>
Sindhu S <sindhus@live.in>
Slava Semushin <semushin@redhat.com>
Solomon Hykes <solomon@docker.com>
Song Gao <song@gao.io>
Spencer Brown <spencer@spencerbrown.org>
Spring Lee <xi.shuai@outlook.com>
squeegels <lmscrewy@gmail.com>
squeegels <1674195+squeegels@users.noreply.github.com>
Srini Brahmaroutu <srbrahma@us.ibm.com>
Stefan S. <tronicum@user.github.com>
Stefan Scherer <stefan.scherer@docker.com>
@ -779,7 +608,6 @@ Stephen Rust <srust@blockbridge.com>
Steve Durrheimer <s.durrheimer@gmail.com>
Steve Richards <steve.richards@docker.com>
Steven Burgess <steven.a.burgess@hotmail.com>
Stoica-Marcu Floris-Andrei <floris.sm@gmail.com>
Subhajit Ghosh <isubuz.g@gmail.com>
Sun Jianbo <wonderflow.sun@gmail.com>
Sune Keller <absukl@almbrand.dk>
@ -791,15 +619,9 @@ Sébastien HOUZÉ <cto@verylastroom.com>
T K Sourabh <sourabhtk37@gmail.com>
TAGOMORI Satoshi <tagomoris@gmail.com>
taiji-tech <csuhqg@foxmail.com>
Takeshi Koenuma <t.koenuma2@gmail.com>
Takuya Noguchi <takninnovationresearch@gmail.com>
Taylor Jones <monitorjbl@gmail.com>
Teiva Harsanyi <t.harsanyi@thebeat.co>
Tejaswini Duggaraju <naduggar@microsoft.com>
Tengfei Wang <tfwang@alauda.io>
Teppei Fukuda <knqyf263@gmail.com>
Thatcher Peskens <thatcher@docker.com>
Thibault Coupin <thibault.coupin@gmail.com>
Thomas Gazagnaire <thomas@gazagnaire.org>
Thomas Krzero <thomas.kovatchitch@gmail.com>
Thomas Leonard <thomas.leonard@docker.com>
@ -811,10 +633,8 @@ Tianyi Wang <capkurmagati@gmail.com>
Tibor Vass <teabee89@gmail.com>
Tim Dettrick <t.dettrick@uq.edu.au>
Tim Hockin <thockin@google.com>
Tim Sampson <tim@sampson.fi>
Tim Smith <timbot@google.com>
Tim Waugh <twaugh@redhat.com>
Tim Welsh <timothy.welsh@docker.com>
Tim Wraight <tim.wraight@tangentlabs.co.uk>
timfeirg <kkcocogogo@gmail.com>
Timothy Hobbs <timothyhobbs@seznam.cz>
@ -826,7 +646,6 @@ Tom Fotherby <tom+github@peopleperhour.com>
Tom Klingenberg <tklingenberg@lastflood.net>
Tom Milligan <code@tommilligan.net>
Tom X. Tobin <tomxtobin@tomxtobin.com>
Tomas Bäckman <larstomas@gmail.com>
Tomas Tomecek <ttomecek@redhat.com>
Tomasz Kopczynski <tomek@kopczynski.net.pl>
Tomáš Hrčka <thrcka@redhat.com>
@ -838,18 +657,13 @@ Tristan Carel <tristan@cogniteev.com>
Tycho Andersen <tycho@docker.com>
Tycho Andersen <tycho@tycho.ws>
uhayate <uhayate.gong@daocloud.io>
Ulrich Bareth <ulrich.bareth@gmail.com>
Ulysses Souza <ulysses.souza@docker.com>
Umesh Yadav <umesh4257@gmail.com>
Vaclav Struhar <struharv@gmail.com>
Valentin Lorentz <progval+git@progval.net>
Vardan Pogosian <vardan.pogosyan@gmail.com>
Venkateswara Reddy Bukkasamudram <bukkasamudram@outlook.com>
Veres Lajos <vlajos@gmail.com>
Victor Vieux <victor.vieux@docker.com>
Victoria Bialas <victoria.bialas@docker.com>
Viktor Stanchev <me@viktorstanchev.com>
Ville Skyttä <ville.skytta@iki.fi>
Vimal Raghubir <vraghubir0418@gmail.com>
Vincent Batts <vbatts@redhat.com>
Vincent Bernat <Vincent.Bernat@exoscale.ch>
@ -863,7 +677,6 @@ Wang Long <long.wanglong@huawei.com>
Wang Ping <present.wp@icloud.com>
Wang Xing <hzwangxing@corp.netease.com>
Wang Yuexiao <wang.yuexiao@zte.com.cn>
Wang Yumu <37442693@qq.com>
Wataru Ishida <ishida.wataru@lab.ntt.co.jp>
Wayne Song <wsong@docker.com>
Wen Cheng Ma <wenchma@cn.ibm.com>
@ -872,7 +685,6 @@ Wes Morgan <cap10morgan@gmail.com>
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
William Henry <whenry@redhat.com>
Xianglin Gao <xlgao@zju.edu.cn>
Xiaodong Liu <liuxiaodong@loongson.cn>
Xiaodong Zhang <a4012017@sina.com>
Xiaoxi He <xxhe@alauda.io>
Xinbo Weng <xihuanbo_0521@zju.edu.cn>
@ -886,25 +698,19 @@ Yong Tang <yong.tang.github@outlook.com>
Yosef Fertel <yfertel@gmail.com>
Yu Peng <yu.peng36@zte.com.cn>
Yuan Sun <sunyuan3@huawei.com>
Yucheng Wu <wyc123wyc@gmail.com>
Yue Zhang <zy675793960@yeah.net>
Yunxiang Huang <hyxqshk@vip.qq.com>
Zachary Romero <zacromero3@gmail.com>
Zander Mackie <zmackie@gmail.com>
zebrilee <zebrilee@gmail.com>
Zeel B Patel <patel_zeel@iitgn.ac.in>
Zhang Kun <zkazure@gmail.com>
Zhang Wei <zhangwei555@huawei.com>
Zhang Wentao <zhangwentao234@huawei.com>
ZhangHang <stevezhang2014@gmail.com>
zhenghenghuo <zhenghenghuo@zju.edu.cn>
Zhiwei Liang <zliang@akamai.com>
Zhou Hao <zhouhao@cn.fujitsu.com>
Zhoulin Xie <zhoulin.xie@daocloud.io>
Zhu Guihua <zhugh.fnst@cn.fujitsu.com>
Zhuo Zhi <h.dwwwwww@gmail.com>
Álex González <agonzalezro@gmail.com>
Álvaro Lázaro <alvaro.lazaro.g@gmail.com>
Átila Camurça Alves <camurca.home@gmail.com>
Александр Менщиков <__Singleton__@hackerdom.ru>
徐俊杰 <paco.xu@daocloud.io>

View File

@ -1,5 +1,9 @@
# Contributing to Docker
Want to hack on Docker? Awesome! We have a contributor's guide that explains
[setting up a Docker development environment and the contribution
process](https://docs.docker.com/opensource/project/who-written-for/).
This page contains information about reporting issues as well as some tips and
guidelines useful to experienced open source contributors. Finally, make sure
you read our [community guidelines](#docker-community-guidelines) before you
@ -16,9 +20,9 @@ start participating.
## Reporting security issues
The Docker maintainers take security seriously. If you discover a security
issue, bring it to their attention right away!
issue, please bring it to their attention right away!
**DO NOT** file a public issue, instead send your report privately to
Please **DO NOT** file a public issue, instead send your report privately to
[security@docker.com](mailto:security@docker.com).
Security reports are greatly appreciated and we will publicly thank you for it.
@ -39,7 +43,7 @@ If you find a match, you can use the "subscribe" button to get notified on
updates. Do *not* leave random "+1" or "I have this too" comments, as they
only clutter the discussion, and don't help resolving it. However, if you
have ways to reproduce the issue or have additional information that may help
resolving the issue, leave a comment.
resolving the issue, please leave a comment.
When reporting issues, always include:
@ -84,7 +88,7 @@ use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contr
<tr>
<td>Community Slack</td>
<td>
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://dockr.ly/comm-slack" target="_blank">with this link</a>.
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://community.docker.com/registrations/groups/4316" target="_blank">with this link</a>.
</td>
</tr>
<tr>
@ -166,10 +170,10 @@ Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in the pull requ
description that close an issue. Including references automatically closes the issue
on a merge.
Do not add yourself to the `AUTHORS` file, as it is regenerated regularly
Please do not add yourself to the `AUTHORS` file, as it is regenerated regularly
from the Git history.
See the [Coding Style](#coding-style) for further guidelines.
Please see the [Coding Style](#coding-style) for further guidelines.
### Merge approval
@ -188,7 +192,7 @@ For more details, see the [MAINTAINERS](MAINTAINERS) page.
The sign-off is a simple line at the end of the explanation for the patch. Your
signature certifies that you wrote the patch or otherwise have the right to pass
it on as an open-source patch. The rules are pretty simple: if you can certify
the below (from [developercertificate.org](https://developercertificate.org):
the below (from [developercertificate.org](http://developercertificate.org/)):
```
Developer Certificate of Origin
@ -269,8 +273,8 @@ guidelines for the community as a whole:
* Stay on topic: Make sure that you are posting to the correct channel and
avoid off-topic discussions. Remember when you update an issue or respond
to an email you are potentially sending to a large number of people. Consider
this before you update. Also remember that nobody likes spam.
to an email you are potentially sending to a large number of people. Please
consider this before you update. Also remember that nobody likes spam.
* Don't send email to the maintainers: There's no need to send email to the
maintainers to ask them to investigate an issue or to take a look at a
@ -329,11 +333,12 @@ mind when nudging others to comply.
The rules:
1. All code should be formatted with `gofumpt` (preferred) or `gofmt -s`.
1. All code should be formatted with `gofmt -s`.
2. All code should pass the default levels of
[`golint`](https://github.com/golang/lint).
3. All code should follow the guidelines covered in [Effective Go](https://go.dev/doc/effective_go)
and [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments).
3. All code should follow the guidelines covered in [Effective
Go](http://golang.org/doc/effective_go.html) and [Go Code Review
Comments](https://github.com/golang/go/wiki/CodeReviewComments).
4. Comment the code. Tell us the why, the history and the context.
5. Document _all_ declarations and methods, even private ones. Declare
expectations, caveats and anything else that may be important. If a type
@ -355,6 +360,6 @@ The rules:
guidelines. Since you've read all the rules, you now know that.
If you are having trouble getting into the mood of idiomatic Go, we recommend
reading through [Effective Go](https://go.dev/doc/effective_go). The
[Go Blog](https://go.dev/blog/) is also a great resource. Drinking the
kool-aid is a lot easier than going thirsty.
reading through [Effective Go](https://golang.org/doc/effective_go.html). The
[Go Blog](https://blog.golang.org) is also a great resource. Drinking the
kool-aid is a lot easier than going thirsty.

View File

@ -1,134 +0,0 @@
# syntax=docker/dockerfile:1
ARG BASE_VARIANT=alpine
ARG ALPINE_VERSION=3.20
ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.22.7
ARG XX_VERSION=1.5.0
ARG GOVERSIONINFO_VERSION=v1.3.0
ARG GOTESTSUM_VERSION=v1.10.0
ARG BUILDX_VERSION=0.16.1
ARG COMPOSE_VERSION=v2.29.0
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-base-alpine
ENV GOTOOLCHAIN=local
COPY --link --from=xx / /
RUN apk add --no-cache bash clang lld llvm file git
WORKDIR /go/src/github.com/docker/cli
FROM build-base-alpine AS build-alpine
ARG TARGETPLATFORM
# gcc is installed for libgcc only
RUN xx-apk add --no-cache musl-dev gcc
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-${BASE_DEBIAN_DISTRO} AS build-base-debian
ENV GOTOOLCHAIN=local
COPY --link --from=xx / /
RUN apt-get update && apt-get install --no-install-recommends -y bash clang lld llvm file
WORKDIR /go/src/github.com/docker/cli
FROM build-base-debian AS build-debian
ARG TARGETPLATFORM
RUN xx-apt-get install --no-install-recommends -y libc6-dev libgcc-12-dev pkgconf
FROM build-base-${BASE_VARIANT} AS goversioninfo
ARG GOVERSIONINFO_VERSION
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
GOBIN=/out GO111MODULE=on CGO_ENABLED=0 go install "github.com/josephspurrier/goversioninfo/cmd/goversioninfo@${GOVERSIONINFO_VERSION}"
FROM build-base-${BASE_VARIANT} AS gotestsum
ARG GOTESTSUM_VERSION
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
GOBIN=/out GO111MODULE=on CGO_ENABLED=0 go install "gotest.tools/gotestsum@${GOTESTSUM_VERSION}" \
&& /out/gotestsum --version
FROM build-${BASE_VARIANT} AS build
# GO_LINKMODE defines if static or dynamic binary should be produced
ARG GO_LINKMODE=static
# GO_BUILDTAGS defines additional build tags
ARG GO_BUILDTAGS
# GO_STRIP strips debugging symbols if set
ARG GO_STRIP
# CGO_ENABLED manually sets if cgo is used
ARG CGO_ENABLED
# VERSION sets the version for the produced binary
ARG VERSION
# PACKAGER_NAME sets the company that produced the windows binary
ARG PACKAGER_NAME
COPY --link --from=goversioninfo /out/goversioninfo /usr/bin/goversioninfo
RUN --mount=type=bind,target=.,ro \
--mount=type=cache,target=/root/.cache \
--mount=type=tmpfs,target=cli/winresources \
# override the default behavior of go with xx-go
xx-go --wrap && \
# export GOCACHE=$(go env GOCACHE)/$(xx-info)$([ -f /etc/alpine-release ] && echo "alpine") && \
TARGET=/out ./scripts/build/binary && \
xx-verify $([ "$GO_LINKMODE" = "static" ] && echo "--static") /out/docker
FROM build-${BASE_VARIANT} AS test
COPY --link --from=gotestsum /out/gotestsum /usr/bin/gotestsum
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/')
FROM scratch AS test-coverage
COPY --from=test /tmp/coverage.txt /coverage.txt
FROM build-${BASE_VARIANT} AS build-plugins
ARG GO_LINKMODE=static
ARG GO_BUILDTAGS
ARG GO_STRIP
ARG CGO_ENABLED
ARG VERSION
RUN --mount=ro --mount=type=cache,target=/root/.cache \
xx-go --wrap && \
TARGET=/out ./scripts/build/plugins e2e/cli-plugins/plugins/*
FROM build-base-alpine AS e2e-base-alpine
RUN apk add --no-cache build-base curl openssl openssh-client
FROM build-base-debian AS e2e-base-debian
RUN apt-get update && apt-get install -y build-essential curl openssl openssh-client
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/
COPY --link --from=buildx /buildx /usr/libexec/docker/cli-plugins/docker-buildx
COPY --link --from=compose /docker-compose /usr/libexec/docker/cli-plugins/docker-compose
COPY --link . .
ENV DOCKER_BUILDKIT=1
ENV PATH=/go/src/github.com/docker/cli/build:$PATH
CMD ./scripts/test/e2e/entry
FROM build-base-${BASE_VARIANT} AS dev
COPY --link . .
FROM scratch AS plugins
COPY --from=build-plugins /out .
FROM scratch AS bin-image-linux
COPY --from=build /out/docker /docker
FROM scratch AS bin-image-darwin
COPY --from=build /out/docker /docker
FROM scratch AS bin-image-windows
COPY --from=build /out/docker /docker.exe
FROM bin-image-${TARGETOS} AS bin-image
FROM scratch AS binary
COPY --from=build /out .

13
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,13 @@
wrappedNode(label: 'linux && x86_64', cleanWorkspace: true) {
timeout(time: 60, unit: 'MINUTES') {
stage "Git Checkout"
checkout scm
stage "Run end-to-end test suite"
sh "docker version"
sh "docker info"
sh "E2E_UNIQUE_ID=clie2e${BUILD_NUMBER} \
IMAGE_TAG=clie2e${BUILD_NUMBER} \
DOCKER_BUILDKIT=1 make -f docker.Makefile test-e2e"
}
}

View File

@ -22,9 +22,11 @@
# subsystem maintainers accountable. If ownership is unclear, they are the de facto owners.
people = [
"aaronlehmann",
"albers",
"cpuguy83",
"rumpl",
"dnephin",
"justincormack",
"silvin-lubecki",
"stevvooe",
"thajeztah",
@ -34,6 +36,14 @@
"vieux",
]
[Org."Docs maintainers"]
# TODO Describe the docs maintainers role.
people = [
"thajeztah"
]
[Org.Curators]
# The curators help ensure that incoming issues and pull requests are properly triaged and
@ -47,26 +57,8 @@
# - close an issue or pull request when it's inappropriate or off-topic
people = [
"bsousaa",
"neersighted",
"programmerq",
"sam-thibault",
"vvoland"
]
[Org.Alumni]
# This list contains maintainers that are no longer active on the project.
# It is thanks to these people that the project has become what it is today.
# Thank you!
people = [
# Before becoming a maintainer, Daniel Nephin was a core contributor
# to "Fig" (now known as Docker Compose). As a maintainer for both the
# Engine and Docker CLI, Daniel contributed many features, among which
# the `docker stack` commands, allowing users to deploy their Docker
# Compose projects as a Swarm service.
"dnephin",
"thajeztah"
]
[people]
@ -77,16 +69,16 @@
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
[people.aaronlehmann]
Name = "Aaron Lehmann"
Email = "aaron.lehmann@docker.com"
GitHub = "aaronlehmann"
[people.albers]
Name = "Harald Albers"
Email = "github@albersweb.de"
GitHub = "albers"
[people.bsousaa]
Name = "Bruno de Sousa"
Email = "bruno.sousa@docker.com"
GitHub = "bsousaa"
[people.cpuguy83]
Name = "Brian Goff"
Email = "cpuguy83@gmail.com"
@ -97,26 +89,16 @@
Email = "dnephin@gmail.com"
GitHub = "dnephin"
[people.neersighted]
Name = "Bjorn Neergaard"
Email = "bneergaard@mirantis.com"
GitHub = "neersighted"
[people.justincormack]
Name = "Justin Cormack"
Email = "justin.cormack@docker.com"
GitHub = "justincormack"
[people.programmerq]
Name = "Jeff Anderson"
Email = "jeff@docker.com"
GitHub = "programmerq"
[people.rumpl]
Name = "Djordje Lukic"
Email = "djordje.lukic@docker.com"
GitHub = "rumpl"
[people.sam-thibault]
Name = "Sam Thibault"
Email = "sam.thibault@docker.com"
GitHub = "sam-thibault"
[people.silvin-lubecki]
Name = "Silvin Lubecki"
Email = "silvin.lubecki@docker.com"
@ -152,8 +134,3 @@
Email = "vieux@docker.com"
GitHub = "vieux"
[people.vvoland]
Name = "Paweł Gronowski"
Email = "pawel.gronowski@docker.com"
GitHub = "vvoland"

127
Makefile
View File

@ -1,113 +1,102 @@
#
# github.com/docker/cli
#
# Sets the name of the company that produced the windows binary.
PACKAGER_NAME ?=
# The repository doesn't have a go.mod, but "go list", and "gotestsum"
# expect to be run from a module.
GO111MODULE=auto
export GO111MODULE
all: binary
_:=$(shell ./scripts/warn-outside-container $(MAKECMDGOALS))
.PHONY: dev
dev: ## start a build container in interactive mode for in-container development
@if [ -n "${DISABLE_WARN_OUTSIDE_CONTAINER}" ]; then \
echo "you are already in the dev container"; \
else \
$(MAKE) -f docker.Makefile dev; \
fi
.PHONY: shell
shell: dev ## alias for dev
.PHONY: clean
clean: ## remove build artifacts
rm -rf ./build/* man/man[1-9] docs/yaml
rm -rf ./build/* cli/winresources/rsrc_* ./man/man[1-9] docs/yaml/gen
.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 $(TESTFLAGS) -- $${TESTDIRS:-$(shell go list ./... | grep -vE '/vendor/|/e2e/')}
.PHONY: test
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/')} $(TESTFLAGS)
.PHONY: test-coverage
test-coverage: ## run test coverage
mkdir -p $(CURDIR)/build/coverage
gotestsum -- $(shell go list ./... | grep -vE '/vendor/|/e2e/') -coverprofile=$(CURDIR)/build/coverage/coverage.txt
gotestsum -- -coverprofile=coverage.txt $(shell go list ./... | grep -vE '/vendor/|/e2e/')
.PHONY: fmt
fmt:
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d
.PHONY: lint
lint: ## run all the lint tools
golangci-lint run
.PHONY: shellcheck
shellcheck: ## run shellcheck validation
find scripts/ contrib/completion/bash -type f | grep -v scripts/winresources | grep -v '.*.ps1' | xargs shellcheck
.PHONY: fmt
fmt: ## run gofumpt (if present) or gofmt
@if command -v gofumpt > /dev/null; then \
gofumpt -w -d -lang=1.21 . ; \
else \
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \
fi
gometalinter --config gometalinter.json ./...
.PHONY: binary
binary: ## build executable for Linux
@echo "WARNING: binary creates a Linux executable. Use cross for macOS or Windows."
./scripts/build/binary
.PHONY: dynbinary
dynbinary: ## build dynamically linked binary
GO_LINKMODE=dynamic ./scripts/build/binary
.PHONY: plugins
plugins: ## build example CLI plugins
./scripts/build/plugins
.PHONY: vendor
vendor: ## update vendor with go modules
.PHONY: cross
cross: ## build executable for macOS and Windows
./scripts/build/cross
.PHONY: binary-windows
binary-windows: ## build executable for Windows
./scripts/build/windows
.PHONY: plugins-windows
plugins-windows: ## build example CLI plugins for Windows
./scripts/build/plugins-windows
.PHONY: binary-osx
binary-osx: ## build executable for macOS
./scripts/build/osx
.PHONY: plugins-osx
plugins-osx: ## build example CLI plugins for macOS
./scripts/build/plugins-osx
.PHONY: dynbinary
dynbinary: ## build dynamically linked binary
./scripts/build/dynbinary
vendor: vendor.conf ## check that vendor matches vendor.conf
rm -rf vendor
./scripts/vendor update
.PHONY: validate-vendor
validate-vendor: ## validate vendor
./scripts/vendor validate
.PHONY: mod-outdated
mod-outdated: ## check outdated dependencies
./scripts/vendor outdated
bash -c 'vndr |& grep -v -i clone'
scripts/validate/check-git-diff vendor
.PHONY: authors
authors: ## generate AUTHORS file from git history
scripts/docs/generate-authors.sh
.PHONY: completion
completion: binary
completion: /etc/bash_completion.d/docker
completion: ## generate and install the completion scripts
.PHONY: /etc/bash_completion.d/docker
/etc/bash_completion.d/docker: ## generate and install the bash-completion script
mkdir -p /etc/bash_completion.d
docker completion bash > /etc/bash_completion.d/docker
.PHONY: manpages
manpages: ## generate man pages from go source and markdown
scripts/docs/generate-man.sh
.PHONY: mddocs
mddocs: ## generate markdown files from go source
scripts/docs/generate-md.sh
.PHONY: yamldocs
yamldocs: ## generate documentation YAML files consumed by docs repo
scripts/docs/generate-yaml.sh
.PHONY: shellcheck
shellcheck: ## run shellcheck validation
scripts/validate/shellcheck
.PHONY: help
help: ## print this help
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {gsub("\\\\n",sprintf("\n%22c",""), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
cli/compose/schema/bindata.go: cli/compose/schema/data/*.json
go generate github.com/docker/cli/cli/compose/schema
compose-jsonschema: cli/compose/schema/bindata.go
scripts/validate/check-git-diff cli/compose/schema/bindata.go
.PHONY: ci-validate
ci-validate:
time make -B vendor
time make -B compose-jsonschema
time make manpages
time make yamldocs

4
NOTICE
View File

@ -3,7 +3,7 @@ Copyright 2012-2017 Docker, Inc.
This product includes software developed at Docker, Inc. (https://www.docker.com).
This product contains software (https://github.com/creack/pty) developed
This product contains software (https://github.com/kr/pty) developed
by Keith Rarick, licensed under the MIT License.
The following is courtesy of our legal counsel:
@ -14,6 +14,6 @@ United States and other governments.
It is your responsibility to ensure that your use and/or transfer does not
violate applicable laws.
For more information, see https://www.bis.doc.gov
For more information, please see https://www.bis.doc.gov
See also https://www.apache.org/dev/crypto.html and/or seek legal counsel.

View File

@ -1,73 +1,58 @@
# Docker CLI
[![build status](https://circleci.com/gh/docker/cli.svg?style=shield)](https://circleci.com/gh/docker/cli/tree/master) [![Build Status](https://jenkins.dockerproject.org/job/docker/job/cli/job/master/badge/icon)](https://jenkins.dockerproject.org/job/docker/job/cli/job/master/)
[![PkgGoDev](https://img.shields.io/badge/go.dev-docs-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/docker/cli)
[![Build Status](https://img.shields.io/github/actions/workflow/status/docker/cli/build.yml?branch=master&label=build&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Abuild)
[![Test Status](https://img.shields.io/github/actions/workflow/status/docker/cli/test.yml?branch=master&label=test&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Atest)
[![Go Report Card](https://goreportcard.com/badge/github.com/docker/cli)](https://goreportcard.com/report/github.com/docker/cli)
[![Codecov](https://img.shields.io/codecov/c/github/docker/cli?logo=codecov)](https://codecov.io/gh/docker/cli)
docker/cli
==========
## About
This repository is the home of the cli used in the Docker CE and
Docker EE products.
This repository is the home of the Docker CLI.
## Development
Development
===========
`docker/cli` is developed using Docker.
Build CLI from source:
Build a linux binary:
```shell
docker buildx bake
```
$ make -f docker.Makefile binary
```
Build binaries for all supported platforms:
```shell
docker buildx bake cross
```
Build for a specific platform:
```shell
docker buildx bake --set binary.platform=linux/arm64
```
Build dynamic binary for glibc or musl:
```shell
USE_GLIBC=1 docker buildx bake dynbinary
$ make -f docker.Makefile cross
```
Run all linting:
```shell
docker buildx bake lint shellcheck
```
Run test:
```shell
docker buildx bake test
$ make -f docker.Makefile lint
```
List all the available targets:
```shell
make help
```
$ make help
```
### In-container development environment
Start an interactive development environment:
```shell
make -f docker.Makefile shell
```
$ make -f docker.Makefile shell
```
## Legal
In the development environment you can run many tasks, including build binaries:
```
$ make binary
```
Legal
=====
*Brought to you courtesy of our legal counsel. For more context,
see the [NOTICE](https://github.com/docker/cli/blob/master/NOTICE) document in this repo.*
please see the [NOTICE](https://github.com/docker/cli/blob/master/NOTICE) document in this repo.*
Use and transfer of Docker may be subject to certain restrictions by the
United States and other governments.
@ -75,10 +60,10 @@ United States and other governments.
It is your responsibility to ensure that your use and/or transfer does not
violate applicable laws.
For more information, see https://www.bis.doc.gov
## Licensing
For more information, please see https://www.bis.doc.gov
Licensing
=========
docker/cli is licensed under the Apache License, Version 2.0. See
[LICENSE](https://github.com/docker/docker/blob/master/LICENSE) for the full
license text.

View File

@ -80,6 +80,6 @@ End-to-end test should run the `docker` binary using
and make assertions about the exit code, stdout, stderr, and local file system.
Any Docker image or registry operations should use `registry:5000/<image name>`
to communicate with the local instance of the registry. To load
to communicate with the local instance of the Docker registry. To load
additional fixture images to the registry see
[scripts/test/e2e/run](https://github.com/docker/cli/blob/master/scripts/test/e2e/run).

View File

@ -1 +1 @@
27.0.1-dev
19.03.0-dev

23
appveyor.yml Normal file
View File

@ -0,0 +1,23 @@
version: "{build}"
clone_folder: c:\gopath\src\github.com\docker\cli
environment:
GOPATH: c:\gopath
GOVERSION: 1.12.10
DEPVERSION: v0.4.1
install:
- rmdir c:\go /s /q
- appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-amd64.msi
- msiexec /i go%GOVERSION%.windows-amd64.msi /q
- go version
- go env
deploy: false
build_script:
- ps: .\scripts\make.ps1 -Binary
test_script:
- ps: .\scripts\make.ps1 -TestUnit

133
circle.yml Normal file
View File

@ -0,0 +1,133 @@
version: 2
jobs:
lint:
working_directory: /work
docker: [{image: 'docker:18.09-git'}]
environment:
DOCKER_BUILDKIT: 1
steps:
- checkout
- setup_remote_docker:
version: 18.09.3
reusable: true
exclusive: false
- run:
command: docker version
- run:
name: "Lint"
command: |
docker build --progress=plain -f dockerfiles/Dockerfile.lint --tag cli-linter:$CIRCLE_BUILD_NUM .
docker run --rm cli-linter:$CIRCLE_BUILD_NUM
cross:
working_directory: /work
docker: [{image: 'docker:18.09-git'}]
environment:
DOCKER_BUILDKIT: 1
parallelism: 3
steps:
- checkout
- setup_remote_docker:
version: 18.09.3
reusable: true
exclusive: false
- run:
name: "Cross"
command: |
docker build --progress=plain -f dockerfiles/Dockerfile.cross --tag cli-builder:$CIRCLE_BUILD_NUM .
name=cross-$CIRCLE_BUILD_NUM-$CIRCLE_NODE_INDEX
docker run \
-e CROSS_GROUP=$CIRCLE_NODE_INDEX \
--name $name cli-builder:$CIRCLE_BUILD_NUM \
make cross
docker cp \
$name:/go/src/github.com/docker/cli/build \
/work/build
- store_artifacts:
path: /work/build
test:
working_directory: /work
docker: [{image: 'docker:18.09-git'}]
environment:
DOCKER_BUILDKIT: 1
steps:
- checkout
- setup_remote_docker:
version: 18.09.3
reusable: true
exclusive: false
- run:
name: "Unit Test with Coverage"
command: |
mkdir -p test-results/unit-tests
docker build --progress=plain -f dockerfiles/Dockerfile.dev --tag cli-builder:$CIRCLE_BUILD_NUM .
docker run \
-e GOTESTSUM_JUNITFILE=/tmp/junit.xml \
--name \
test-$CIRCLE_BUILD_NUM cli-builder:$CIRCLE_BUILD_NUM \
make test-coverage
docker cp \
test-$CIRCLE_BUILD_NUM:/tmp/junit.xml \
./test-results/unit-tests/junit.xml
- run:
name: "Upload to Codecov"
command: |
docker cp \
test-$CIRCLE_BUILD_NUM:/go/src/github.com/docker/cli/coverage.txt \
coverage.txt
apk add -U bash curl
curl -s https://codecov.io/bash | bash || \
echo 'Codecov failed to upload'
- store_test_results:
path: test-results
- store_artifacts:
path: test-results
validate:
working_directory: /work
docker: [{image: 'docker:18.09-git'}]
environment:
DOCKER_BUILDKIT: 1
steps:
- checkout
- setup_remote_docker:
version: 18.09.3
reusable: true
exclusive: false
- run:
name: "Validate Vendor, Docs, and Code Generation"
command: |
rm -f .dockerignore # include .git
docker build --progress=plain -f dockerfiles/Dockerfile.dev --tag cli-builder-with-git:$CIRCLE_BUILD_NUM .
docker run --rm cli-builder-with-git:$CIRCLE_BUILD_NUM \
make ci-validate
no_output_timeout: 15m
shellcheck:
working_directory: /work
docker: [{image: 'docker:18.09-git'}]
environment:
DOCKER_BUILDKIT: 1
steps:
- checkout
- setup_remote_docker:
version: 18.09.3
reusable: true
exclusive: false
- run:
name: "Run shellcheck"
command: |
docker build --progress=plain -f dockerfiles/Dockerfile.shellcheck --tag cli-validator:$CIRCLE_BUILD_NUM .
docker run --rm cli-validator:$CIRCLE_BUILD_NUM \
make shellcheck
workflows:
version: 2
ci:
jobs:
- lint
- cross
- test
- validate
- shellcheck

View File

@ -45,8 +45,8 @@ func main() {
}
var (
who, optContext string
preRun, debug bool
who, context string
preRun, debug bool
)
cmd := &cobra.Command{
Use: "helloworld",
@ -65,7 +65,7 @@ func main() {
fmt.Fprintf(dockerCli.Err(), "Plugin debug mode enabled")
}
switch optContext {
switch context {
case "Christmas":
fmt.Fprintf(dockerCli.Out(), "Merry Christmas!\n")
return nil
@ -92,7 +92,7 @@ func main() {
// These are intended to deliberately clash with the CLIs own top
// level arguments.
flags.BoolVarP(&debug, "debug", "D", false, "Enable debug")
flags.StringVarP(&optContext, "context", "c", "", "Is it Christmas?")
flags.StringVarP(&context, "context", "c", "", "Is it Christmas?")
cmd.AddCommand(goodbye, apiversion, exitStatus2)
return cmd
@ -101,5 +101,6 @@ func main() {
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: "testing",
Experimental: os.Getenv("HELLO_EXPERIMENTAL") != "",
})
}

View File

@ -1,18 +0,0 @@
package hooks
import (
"fmt"
"io"
"github.com/morikuni/aec"
)
func PrintNextSteps(out io.Writer, messages []string) {
if len(messages) == 0 {
return
}
fmt.Fprintln(out, aec.Bold.Apply("\nWhat's next:"))
for _, n := range messages {
_, _ = fmt.Fprintf(out, " %s\n", n)
}
}

View File

@ -1,38 +0,0 @@
package hooks
import (
"bytes"
"testing"
"github.com/morikuni/aec"
"gotest.tools/v3/assert"
)
func TestPrintHookMessages(t *testing.T) {
testCases := []struct {
messages []string
expectedOutput string
}{
{
messages: []string{},
expectedOutput: "",
},
{
messages: []string{"Bork!"},
expectedOutput: aec.Bold.Apply("\nWhat's next:") + "\n" +
" Bork!\n",
},
{
messages: []string{"Foo", "bar"},
expectedOutput: aec.Bold.Apply("\nWhat's next:") + "\n" +
" Foo\n" +
" bar\n",
},
}
for _, tc := range testCases {
w := bytes.Buffer{}
PrintNextSteps(&w, tc.messages)
assert.Equal(t, w.String(), tc.expectedOutput)
}
}

View File

@ -1,116 +0,0 @@
package hooks
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
"text/template"
"github.com/spf13/cobra"
)
type HookType int
const (
NextSteps = iota
)
// HookMessage represents a plugin hook response. Plugins
// declaring support for CLI hooks need to print a json
// representation of this type when their hook subcommand
// is invoked.
type HookMessage struct {
Type HookType
Template string
}
// TemplateReplaceSubcommandName returns a hook template string
// that will be replaced by the CLI subcommand being executed
//
// Example:
//
// "you ran the subcommand: " + TemplateReplaceSubcommandName()
//
// when being executed after the command:
// `docker run --name "my-container" alpine`
// will result in the message:
// `you ran the subcommand: run`
func TemplateReplaceSubcommandName() string {
return hookTemplateCommandName
}
// TemplateReplaceFlagValue returns a hook template string
// that will be replaced by the flags value.
//
// Example:
//
// "you ran a container named: " + TemplateReplaceFlagValue("name")
//
// when being executed after the command:
// `docker run --name "my-container" alpine`
// will result in the message:
// `you ran a container named: my-container`
func TemplateReplaceFlagValue(flag string) string {
return fmt.Sprintf(hookTemplateFlagValue, flag)
}
// TemplateReplaceArg takes an index i and returns a hook
// template string that the CLI will replace the template with
// the ith argument, after processing the passed flags.
//
// Example:
//
// "run this image with `docker run " + TemplateReplaceArg(0) + "`"
//
// when being executed after the command:
// `docker pull alpine`
// will result in the message:
// "Run this image with `docker run alpine`"
func TemplateReplaceArg(i int) string {
return fmt.Sprintf(hookTemplateArg, strconv.Itoa(i))
}
func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
tmpl := template.New("").Funcs(commandFunctions)
tmpl, err := tmpl.Parse(hookTemplate)
if err != nil {
return nil, err
}
b := bytes.Buffer{}
err = tmpl.Execute(&b, cmd)
if err != nil {
return nil, err
}
return strings.Split(b.String(), "\n"), nil
}
var ErrHookTemplateParse = errors.New("failed to parse hook template")
const (
hookTemplateCommandName = "{{.Name}}"
hookTemplateFlagValue = `{{flag . "%s"}}`
hookTemplateArg = "{{arg . %s}}"
)
var commandFunctions = template.FuncMap{
"flag": getFlagValue,
"arg": getArgValue,
}
func getFlagValue(cmd *cobra.Command, flag string) (string, error) {
cmdFlag := cmd.Flag(flag)
if cmdFlag == nil {
return "", ErrHookTemplateParse
}
return cmdFlag.Value.String(), nil
}
func getArgValue(cmd *cobra.Command, i int) (string, error) {
flags := cmd.Flags()
if flags == nil {
return "", ErrHookTemplateParse
}
return flags.Arg(i), nil
}

View File

@ -1,86 +0,0 @@
package hooks
import (
"testing"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
)
func TestParseTemplate(t *testing.T) {
type testFlag struct {
name string
value string
}
testCases := []struct {
template string
flags []testFlag
args []string
expectedOutput []string
}{
{
template: "",
expectedOutput: []string{""},
},
{
template: "a plain template message",
expectedOutput: []string{"a plain template message"},
},
{
template: TemplateReplaceFlagValue("tag"),
flags: []testFlag{
{
name: "tag",
value: "my-tag",
},
},
expectedOutput: []string{"my-tag"},
},
{
template: TemplateReplaceFlagValue("test-one") + " " + TemplateReplaceFlagValue("test2"),
flags: []testFlag{
{
name: "test-one",
value: "value",
},
{
name: "test2",
value: "value2",
},
},
expectedOutput: []string{"value value2"},
},
{
template: TemplateReplaceArg(0) + " " + TemplateReplaceArg(1),
args: []string{"zero", "one"},
expectedOutput: []string{"zero one"},
},
{
template: "You just pulled " + TemplateReplaceArg(0),
args: []string{"alpine"},
expectedOutput: []string{"You just pulled alpine"},
},
{
template: "one line\nanother line!",
expectedOutput: []string{"one line", "another line!"},
},
}
for _, tc := range testCases {
testCmd := &cobra.Command{
Use: "pull",
Args: cobra.ExactArgs(len(tc.args)),
}
for _, f := range tc.flags {
_ = testCmd.Flags().String(f.name, "", "")
err := testCmd.Flag(f.name).Value.Set(f.value)
assert.NilError(t, err)
}
err := testCmd.Flags().Parse(tc.args)
assert.NilError(t, err)
out, err := ParseTemplate(tc.template, testCmd)
assert.NilError(t, err)
assert.DeepEqual(t, out, tc.expectedOutput)
}
}

View File

@ -1,6 +1,8 @@
package manager
import "os/exec"
import (
"os/exec"
)
// Candidate represents a possible plugin candidate, for mocking purposes
type Candidate interface {

View File

@ -7,14 +7,15 @@ import (
"testing"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
"gotest.tools/assert"
"gotest.tools/assert/cmp"
)
type fakeCandidate struct {
path string
exec bool
meta string
path string
exec bool
meta string
allowExperimental bool
}
func (c *fakeCandidate) Path() string {
@ -29,7 +30,7 @@ func (c *fakeCandidate) Metadata() ([]byte, error) {
}
func TestValidateCandidate(t *testing.T) {
const (
var (
goodPluginName = NamePrefix + "goodplugin"
builtinName = NamePrefix + "builtin"
@ -69,20 +70,21 @@ func TestValidateCandidate(t *testing.T) {
{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"},
{name: "experimental required", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental}, invalid: "requires experimental CLI"},
// 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}},
{name: "valid + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`, allowExperimental: true}},
{name: "experimental + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental, allowExperimental: true}},
} {
t.Run(tc.name, func(t *testing.T) {
p, err := newPlugin(tc.c, fakeroot.Commands())
switch {
case tc.err != "":
p, err := newPlugin(tc.c, fakeroot, tc.c.allowExperimental)
if tc.err != "" {
assert.ErrorContains(t, err, tc.err)
case tc.invalid != "":
} else if tc.invalid != "" {
assert.NilError(t, err)
assert.Assert(t, cmp.ErrorType(p.Err, reflect.TypeOf(&pluginError{})))
assert.ErrorContains(t, p.Err, tc.invalid)
default:
} else {
assert.NilError(t, err)
assert.Equal(t, NamePrefix+p.Name, goodPluginName)
assert.Equal(t, p.SchemaVersion, "0.1.0")

View File

@ -1,15 +1,8 @@
package manager
import (
"fmt"
"net/url"
"os"
"strings"
"sync"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel/attribute"
)
const (
@ -33,116 +26,35 @@ const (
// is, one which failed it's candidate test) and contains the
// reason for the failure.
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
// CommandAnnotationPluginCommandPath is added to overwrite the
// command path for a plugin invocation.
CommandAnnotationPluginCommandPath = "com.docker.cli.plugin.command_path"
)
var pluginCommandStubsOnce sync.Once
// AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid
// plugin. The command stubs will have several annotations added, see
// `CommandAnnotationPlugin*`.
func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err error) {
pluginCommandStubsOnce.Do(func() {
var plugins []Plugin
plugins, err = ListPlugins(dockerCli, rootCmd)
if err != nil {
return
}
for _, p := range plugins {
p := p
vendor := p.Vendor
if vendor == "" {
vendor = "unknown"
}
annotations := map[string]string{
CommandAnnotationPlugin: "true",
CommandAnnotationPluginVendor: vendor,
CommandAnnotationPluginVersion: p.Version,
}
if p.Err != nil {
annotations[CommandAnnotationPluginInvalid] = p.Err.Error()
}
rootCmd.AddCommand(&cobra.Command{
Use: p.Name,
Short: p.ShortDescription,
Run: func(_ *cobra.Command, _ []string) {},
Annotations: annotations,
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
flags := rootCmd.PersistentFlags()
flags.SetOutput(nil)
perr := flags.Parse(args)
if perr != nil {
return err
}
if flags.Changed("help") {
cmd.HelpFunc()(rootCmd, args)
return nil
}
return fmt.Errorf("docker: '%s' is not a docker command.\nSee 'docker --help'", cmd.Name())
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// Delegate completion to plugin
cargs := []string{p.Path, cobra.ShellCompRequestCmd, p.Name}
cargs = append(cargs, args...)
cargs = append(cargs, toComplete)
os.Args = cargs
runCommand, runErr := PluginRunCommand(dockerCli, p.Name, cmd)
if runErr != nil {
return nil, cobra.ShellCompDirectiveError
}
runErr = runCommand.Run()
if runErr == nil {
os.Exit(0) // plugin already rendered complete data
}
return nil, cobra.ShellCompDirectiveError
},
})
}
})
return err
}
const (
dockerCliAttributePrefix = attribute.Key("docker.cli")
cobraCommandPath = attribute.Key("cobra.command_path")
)
func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
commandPath := cmd.Annotations[CommandAnnotationPluginCommandPath]
if commandPath == "" {
commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name)
func AddPluginCommandStubs(dockerCli command.Cli, cmd *cobra.Command) error {
plugins, err := ListPlugins(dockerCli, cmd)
if err != nil {
return err
}
attrSet := attribute.NewSet(
cobraCommandPath.String(commandPath),
)
kvs := make([]attribute.KeyValue, 0, attrSet.Len())
for iter := attrSet.Iter(); iter.Next(); {
attr := iter.Attribute()
kvs = append(kvs, attribute.KeyValue{
Key: dockerCliAttributePrefix + "." + attr.Key,
Value: attr.Value,
for _, p := range plugins {
vendor := p.Vendor
if vendor == "" {
vendor = "unknown"
}
annotations := map[string]string{
CommandAnnotationPlugin: "true",
CommandAnnotationPluginVendor: vendor,
CommandAnnotationPluginVersion: p.Version,
}
if p.Err != nil {
annotations[CommandAnnotationPluginInvalid] = p.Err.Error()
}
cmd.AddCommand(&cobra.Command{
Use: p.Name,
Short: p.ShortDescription,
Run: func(_ *cobra.Command, _ []string) {},
Annotations: annotations,
})
}
return attribute.NewSet(kvs...)
}
func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string {
if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 {
// values in environment variables need to be in baggage format
// otel/baggage package can be used after update to v1.22, currently it encodes incorrectly
attrsSlice := make([]string, attrs.Len())
for iter := attrs.Iter(); iter.Next(); {
i, v := iter.IndexedAttribute()
attrsSlice[i] = string(v.Key) + "=" + url.PathEscape(v.Value.AsString())
}
env = append(env, ResourceAttributesEnvvar+"="+strings.Join(attrsSlice, ","))
}
return env
return nil
}

View File

@ -1,6 +1,3 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.21
package manager
import (
@ -28,11 +25,6 @@ func (e *pluginError) Cause() error {
return e.cause
}
// Unwrap provides compatibility for Go 1.13 error chains.
func (e *pluginError) Unwrap() error {
return e.cause
}
// MarshalText marshalls the pluginError into a textual form.
func (e *pluginError) MarshalText() (text []byte, err error) {
return []byte(e.cause.Error()), nil
@ -41,14 +33,11 @@ func (e *pluginError) MarshalText() (text []byte, err error) {
// wrapAsPluginError wraps an error in a pluginError with an
// additional message, analogous to errors.Wrapf.
func wrapAsPluginError(err error, msg string) error {
if err == nil {
return nil
}
return &pluginError{cause: errors.Wrap(err, msg)}
}
// NewPluginError creates a new pluginError, analogous to
// errors.Errorf.
func NewPluginError(msg string, args ...any) error {
func NewPluginError(msg string, args ...interface{}) error {
return &pluginError{cause: errors.Errorf(msg, args...)}
}

View File

@ -1,24 +1,24 @@
package manager
import (
"encoding/json"
"errors"
"fmt"
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
"gotest.tools/assert"
)
func TestPluginError(t *testing.T) {
err := NewPluginError("new error")
assert.Check(t, is.Error(err, "new error"))
assert.Error(t, err, "new error")
inner := errors.New("testing")
inner := fmt.Errorf("testing")
err = wrapAsPluginError(inner, "wrapping")
assert.Check(t, is.Error(err, "wrapping: testing"))
assert.Check(t, is.ErrorIs(err, inner))
assert.Error(t, err, "wrapping: testing")
assert.Equal(t, inner, errors.Cause(err))
actual, err := json.Marshal(err)
assert.Check(t, err)
assert.Check(t, is.Equal(`"wrapping: testing"`, string(actual)))
actual, err := yaml.Marshal(err)
assert.NilError(t, err)
assert.Equal(t, "'wrapping: testing'\n", string(actual))
}

View File

@ -1,199 +0,0 @@
package manager
import (
"context"
"encoding/json"
"strings"
"github.com/docker/cli/cli-plugins/hooks"
"github.com/docker/cli/cli/command"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// HookPluginData is the type representing the information
// that plugins declaring support for hooks get passed when
// being invoked following a CLI command execution.
type HookPluginData struct {
// RootCmd is a string representing the matching hook configuration
// which is currently being invoked. If a hook for `docker context` is
// configured and the user executes `docker context ls`, the plugin will
// be invoked with `context`.
RootCmd string
Flags map[string]string
CommandError string
}
// RunCLICommandHooks is the entrypoint into the hooks execution flow after
// a main CLI command was executed. It calls the hook subcommand for all
// present CLI plugins that declare support for hooks in their metadata and
// parses/prints their responses.
func RunCLICommandHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
flags := getCommandFlags(subCommand)
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, cmdErrorMessage)
}
// RunPluginHooks is the entrypoint for the hooks execution flow
// after a plugin command was just executed by the CLI.
func RunPluginHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) {
commandName := strings.Join(args, " ")
flags := getNaiveFlags(args)
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, "")
}
func runHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
nextSteps := invokeAndCollectHooks(ctx, dockerCli, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
hooks.PrintNextSteps(dockerCli.Err(), nextSteps)
}
func invokeAndCollectHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
// check if the context was cancelled before invoking hooks
select {
case <-ctx.Done():
return nil
default:
}
pluginsCfg := dockerCli.ConfigFile().Plugins
if pluginsCfg == nil {
return nil
}
nextSteps := make([]string, 0, len(pluginsCfg))
for pluginName, cfg := range pluginsCfg {
match, ok := pluginMatch(cfg, subCmdStr)
if !ok {
continue
}
p, err := GetPlugin(pluginName, dockerCli, rootCmd)
if err != nil {
continue
}
hookReturn, err := p.RunHook(ctx, HookPluginData{
RootCmd: match,
Flags: flags,
CommandError: cmdErrorMessage,
})
if err != nil {
// skip misbehaving plugins, but don't halt execution
continue
}
var hookMessageData hooks.HookMessage
err = json.Unmarshal(hookReturn, &hookMessageData)
if err != nil {
continue
}
// currently the only hook type
if hookMessageData.Type != hooks.NextSteps {
continue
}
processedHook, err := hooks.ParseTemplate(hookMessageData.Template, subCmd)
if err != nil {
continue
}
var appended bool
nextSteps, appended = appendNextSteps(nextSteps, processedHook)
if !appended {
logrus.Debugf("Plugin %s responded with an empty hook message %q. Ignoring.", pluginName, string(hookReturn))
}
}
return nextSteps
}
// appendNextSteps appends the processed hook output to the nextSteps slice.
// If the processed hook output is empty, it is not appended.
// Empty lines are not stripped if there's at least one non-empty line.
func appendNextSteps(nextSteps []string, processed []string) ([]string, bool) {
empty := true
for _, l := range processed {
if strings.TrimSpace(l) != "" {
empty = false
break
}
}
if empty {
return nextSteps, false
}
return append(nextSteps, processed...), true
}
// pluginMatch takes a plugin configuration and a string representing the
// command being executed (such as 'image ls' the root 'docker' is omitted)
// and, if the configuration includes a hook for the invoked command, returns
// the configured hook string.
func pluginMatch(pluginCfg map[string]string, subCmd string) (string, bool) {
configuredPluginHooks, ok := pluginCfg["hooks"]
if !ok || configuredPluginHooks == "" {
return "", false
}
commands := strings.Split(configuredPluginHooks, ",")
for _, hookCmd := range commands {
if hookMatch(hookCmd, subCmd) {
return hookCmd, true
}
}
return "", false
}
func hookMatch(hookCmd, subCmd string) bool {
hookCmdTokens := strings.Split(hookCmd, " ")
subCmdTokens := strings.Split(subCmd, " ")
if len(hookCmdTokens) > len(subCmdTokens) {
return false
}
for i, v := range hookCmdTokens {
if v != subCmdTokens[i] {
return false
}
}
return true
}
func getCommandFlags(cmd *cobra.Command) map[string]string {
flags := make(map[string]string)
cmd.Flags().Visit(func(f *pflag.Flag) {
var fValue string
if f.Value.Type() == "bool" {
fValue = f.Value.String()
}
flags[f.Name] = fValue
})
return flags
}
// getNaiveFlags string-matches argv and parses them into a map.
// This is used when calling hooks after a plugin command, since
// in this case we can't rely on the cobra command tree to parse
// flags in this case. In this case, no values are ever passed,
// since we don't have enough information to process them.
func getNaiveFlags(args []string) map[string]string {
flags := make(map[string]string)
for _, arg := range args {
if strings.HasPrefix(arg, "--") {
flags[arg[2:]] = ""
continue
}
if strings.HasPrefix(arg, "-") {
flags[arg[1:]] = ""
}
}
return flags
}

View File

@ -1,143 +0,0 @@
package manager
import (
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestGetNaiveFlags(t *testing.T) {
testCases := []struct {
args []string
expectedFlags map[string]string
}{
{
args: []string{"docker"},
expectedFlags: map[string]string{},
},
{
args: []string{"docker", "build", "-q", "--file", "test.Dockerfile", "."},
expectedFlags: map[string]string{
"q": "",
"file": "",
},
},
{
args: []string{"docker", "--context", "a-context", "pull", "-q", "--progress", "auto", "alpine"},
expectedFlags: map[string]string{
"context": "",
"q": "",
"progress": "",
},
},
}
for _, tc := range testCases {
assert.DeepEqual(t, getNaiveFlags(tc.args), tc.expectedFlags)
}
}
func TestPluginMatch(t *testing.T) {
testCases := []struct {
commandString string
pluginConfig map[string]string
expectedMatch string
expectedOk bool
}{
{
commandString: "image ls",
pluginConfig: map[string]string{
"hooks": "image",
},
expectedMatch: "image",
expectedOk: true,
},
{
commandString: "context ls",
pluginConfig: map[string]string{
"hooks": "build",
},
expectedMatch: "",
expectedOk: false,
},
{
commandString: "context ls",
pluginConfig: map[string]string{
"hooks": "context ls",
},
expectedMatch: "context ls",
expectedOk: true,
},
{
commandString: "image ls",
pluginConfig: map[string]string{
"hooks": "image ls,image",
},
expectedMatch: "image ls",
expectedOk: true,
},
{
commandString: "image ls",
pluginConfig: map[string]string{
"hooks": "",
},
expectedMatch: "",
expectedOk: false,
},
{
commandString: "image inspect",
pluginConfig: map[string]string{
"hooks": "image i",
},
expectedMatch: "",
expectedOk: false,
},
{
commandString: "image inspect",
pluginConfig: map[string]string{
"hooks": "image",
},
expectedMatch: "image",
expectedOk: true,
},
}
for _, tc := range testCases {
match, ok := pluginMatch(tc.pluginConfig, tc.commandString)
assert.Equal(t, ok, tc.expectedOk)
assert.Equal(t, match, tc.expectedMatch)
}
}
func TestAppendNextSteps(t *testing.T) {
testCases := []struct {
processed []string
expectedOut []string
}{
{
processed: []string{},
expectedOut: []string{},
},
{
processed: []string{"", ""},
expectedOut: []string{},
},
{
processed: []string{"Some hint", "", "Some other hint"},
expectedOut: []string{"Some hint", "", "Some other hint"},
},
{
processed: []string{"Hint 1", "Hint 2"},
expectedOut: []string{"Hint 1", "Hint 2"},
},
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
got, appended := appendNextSteps([]string{}, tc.processed)
assert.Check(t, is.DeepEqual(got, tc.expectedOut))
assert.Check(t, is.Equal(appended, len(got) > 0))
})
}
}

View File

@ -1,33 +1,23 @@
package manager
import (
"context"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"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.
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
// ResourceAttributesEnvvar is the name of the envvar that includes additional
// resource attributes for OTEL.
ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES"
)
// ReexecEnvvar is the name of an ennvar which is set to the command
// used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI.
const ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
// errPluginNotFound is the error returned when a plugin could not be found.
type errPluginNotFound string
@ -38,6 +28,16 @@ func (e errPluginNotFound) Error() string {
return "Error: No such CLI plugin: " + string(e)
}
type errPluginRequireExperimental string
// Note: errPluginRequireExperimental implements notFound so that the plugin
// is skipped when listing the plugins.
func (e errPluginRequireExperimental) NotFound() {}
func (e errPluginRequireExperimental) Error() string {
return fmt.Sprintf("plugin candidate %q: requires experimental CLI", string(e))
}
type notFound interface{ NotFound() }
// IsNotFound is true if the given error is due to a plugin not being found.
@ -49,20 +49,10 @@ func IsNotFound(err error) bool {
return ok
}
// getPluginDirs returns the platform-specific locations to search for plugins
// in order of preference.
//
// Plugin-discovery is performed in the following order of preference:
//
// 1. The "cli-plugins" directory inside the CLIs [config.Path] (usually "~/.docker/cli-plugins").
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs].
// 3. Platform-specific defaultSystemPluginDirs.
//
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
func getPluginDirs(cfg *configfile.ConfigFile) ([]string, error) {
func getPluginDirs(dockerCli command.Cli) ([]string, error) {
var pluginDirs []string
if cfg != nil {
if cfg := dockerCli.ConfigFile(); cfg != nil {
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
}
pluginDir, err := config.Path("cli-plugins")
@ -76,12 +66,12 @@ func getPluginDirs(cfg *configfile.ConfigFile) ([]string, error) {
}
func addPluginCandidatesFromDir(res map[string][]string, d string) error {
dentries, err := os.ReadDir(d)
dentries, err := ioutil.ReadDir(d)
if err != nil {
return err
}
for _, dentry := range dentries {
switch dentry.Type() & os.ModeType {
switch dentry.Mode() & os.ModeType {
case 0, os.ModeSymlink:
// Regular file or symlink, keep going
default:
@ -123,39 +113,9 @@ func listPluginCandidates(dirs []string) (map[string][]string, error) {
return result, nil
}
// GetPlugin returns a plugin on the system by its name
func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plugin, error) {
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
if err != nil {
return nil, err
}
candidates, err := listPluginCandidates(pluginDirs)
if err != nil {
return nil, err
}
if paths, ok := candidates[name]; ok {
if len(paths) == 0 {
return nil, errPluginNotFound(name)
}
c := &candidate{paths[0]}
p, err := newPlugin(c, rootcmd.Commands())
if err != nil {
return nil, err
}
if !IsNotFound(p.Err) {
p.ShadowedPaths = paths[1:]
}
return &p, nil
}
return nil, errPluginNotFound(name)
}
// ListPlugins produces a list of the plugins available on the system
func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
pluginDirs, err := getPluginDirs(dockerCli)
if err != nil {
return nil, err
}
@ -166,37 +126,20 @@ func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error
}
var plugins []Plugin
var mu sync.Mutex
eg, _ := errgroup.WithContext(context.TODO())
cmds := rootcmd.Commands()
for _, paths := range candidates {
func(paths []string) {
eg.Go(func() error {
if len(paths) == 0 {
return nil
}
c := &candidate{paths[0]}
p, err := newPlugin(c, cmds)
if err != nil {
return err
}
if !IsNotFound(p.Err) {
p.ShadowedPaths = paths[1:]
mu.Lock()
defer mu.Unlock()
plugins = append(plugins, p)
}
return nil
})
}(paths)
if len(paths) == 0 {
continue
}
c := &candidate{paths[0]}
p, err := newPlugin(c, rootcmd, dockerCli.ClientInfo().HasExperimental)
if err != nil {
return nil, err
}
if !IsNotFound(p.Err) {
p.ShadowedPaths = paths[1:]
plugins = append(plugins, p)
}
}
if err := eg.Wait(); err != nil {
return nil, err
}
sort.Slice(plugins, func(i, j int) bool {
return sortorder.NaturalLess(plugins[i].Name, plugins[j].Name)
})
return plugins, nil
}
@ -215,7 +158,7 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
return nil, errPluginNotFound(name)
}
exename := addExeSuffix(NamePrefix + name)
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
pluginDirs, err := getPluginDirs(dockerCli)
if err != nil {
return nil, err
}
@ -232,12 +175,19 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
}
c := &candidate{path: path}
plugin, err := newPlugin(c, rootcmd.Commands())
plugin, err := newPlugin(c, rootcmd, dockerCli.ClientInfo().HasExperimental)
if err != nil {
return nil, err
}
if plugin.Err != nil {
// TODO: why are we not returning plugin.Err?
err := plugin.Err.(*pluginError).Cause()
// if an experimental plugin was invoked directly while experimental mode is off
// provide a more useful error message than "not found".
if err, ok := err.(errPluginRequireExperimental); ok {
return nil, err
}
return nil, errPluginNotFound(name)
}
cmd := exec.Command(plugin.Path, args...)
@ -250,15 +200,10 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = append(cmd.Environ(), ReexecEnvvar+"="+os.Args[0])
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
return cmd, nil
}
return nil, errPluginNotFound(name)
}
// IsPluginCommand checks if the given cmd is a plugin-stub.
func IsPluginCommand(cmd *cobra.Command) bool {
return cmd.Annotations[CommandAnnotationPlugin] == "true"
}

View File

@ -7,9 +7,8 @@ import (
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
"gotest.tools/v3/fs"
"gotest.tools/assert"
"gotest.tools/fs"
)
func TestListPluginCandidates(t *testing.T) {
@ -46,7 +45,7 @@ func TestListPluginCandidates(t *testing.T) {
)
defer dir.Remove()
dirs := make([]string, 0, 6)
var dirs []string
for _, d := range []string{"plugins1", "nonexistent", "plugins2", "plugins3", "plugins4", "plugins5"} {
dirs = append(dirs, dir.Join(d))
}
@ -82,57 +81,6 @@ func TestListPluginCandidates(t *testing.T) {
assert.DeepEqual(t, candidates, exp)
}
func TestGetPlugin(t *testing.T) {
dir := fs.NewDir(t, t.Name(),
fs.WithFile("docker-bbb", `
#!/bin/sh
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
fs.WithFile("docker-aaa", `
#!/bin/sh
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
)
defer dir.Remove()
cli := test.NewFakeCli(nil)
cli.SetConfigFile(&configfile.ConfigFile{CLIPluginsExtraDirs: []string{dir.Path()}})
plugin, err := GetPlugin("bbb", cli, &cobra.Command{})
assert.NilError(t, err)
assert.Equal(t, plugin.Name, "bbb")
_, err = GetPlugin("ccc", cli, &cobra.Command{})
assert.Error(t, err, "Error: No such CLI plugin: ccc")
assert.Assert(t, IsNotFound(err))
}
func TestListPluginsIsSorted(t *testing.T) {
dir := fs.NewDir(t, t.Name(),
fs.WithFile("docker-bbb", `
#!/bin/sh
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
fs.WithFile("docker-aaa", `
#!/bin/sh
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
)
defer dir.Remove()
cli := test.NewFakeCli(nil)
cli.SetConfigFile(&configfile.ConfigFile{CLIPluginsExtraDirs: []string{dir.Path()}})
plugins, err := ListPlugins(cli, &cobra.Command{})
assert.NilError(t, err)
// We're only interested in the plugins we created for testing this, and only
// if they appear in the expected order
var names []string
for _, p := range plugins {
if p.Name == "aaa" || p.Name == "bbb" {
names = append(names, p.Name)
}
}
assert.DeepEqual(t, names, []string{"aaa", "bbb"})
}
func TestErrPluginNotFound(t *testing.T) {
var err error = errPluginNotFound("test")
err.(errPluginNotFound).NotFound()
@ -149,7 +97,7 @@ func TestGetPluginDirs(t *testing.T) {
expected := append([]string{pluginDir}, defaultSystemPluginDirs...)
var pluginDirs []string
pluginDirs, err = getPluginDirs(cli.ConfigFile())
pluginDirs, err = getPluginDirs(cli)
assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))
assert.NilError(t, err)
@ -160,7 +108,7 @@ func TestGetPluginDirs(t *testing.T) {
cli.SetConfigFile(&configfile.ConfigFile{
CLIPluginsExtraDirs: extras,
})
pluginDirs, err = getPluginDirs(cli.ConfigFile())
pluginDirs, err = getPluginDirs(cli)
assert.DeepEqual(t, expected, pluginDirs)
assert.NilError(t, err)
}

View File

@ -1,20 +1,8 @@
//go:build !windows
// +build !windows
package manager
// defaultSystemPluginDirs are the platform-specific locations to search
// for plugins in order of preference.
//
// Plugin-discovery is performed in the following order of preference:
//
// 1. The "cli-plugins" directory inside the CLIs config-directory (usually "~/.docker/cli-plugins").
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs].
// 3. Platform-specific defaultSystemPluginDirs (as defined below).
//
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
var defaultSystemPluginDirs = []string{
"/usr/local/lib/docker/cli-plugins",
"/usr/local/libexec/docker/cli-plugins",
"/usr/lib/docker/cli-plugins",
"/usr/libexec/docker/cli-plugins",
"/usr/local/lib/docker/cli-plugins", "/usr/local/libexec/docker/cli-plugins",
"/usr/lib/docker/cli-plugins", "/usr/libexec/docker/cli-plugins",
}

View File

@ -5,16 +5,6 @@ import (
"path/filepath"
)
// defaultSystemPluginDirs are the platform-specific locations to search
// for plugins in order of preference.
//
// Plugin-discovery is performed in the following order of preference:
//
// 1. The "cli-plugins" directory inside the CLIs config-directory (usually "~/.docker/cli-plugins").
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs].
// 3. Platform-specific defaultSystemPluginDirs (as defined below).
//
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
var defaultSystemPluginDirs = []string{
filepath.Join(os.Getenv("ProgramData"), "Docker", "cli-plugins"),
filepath.Join(os.Getenv("ProgramFiles"), "Docker", "cli-plugins"),

View File

@ -8,14 +8,9 @@ const (
// which must be supported by every plugin and returns the
// plugin metadata.
MetadataSubcommandName = "docker-cli-plugin-metadata"
// HookSubcommandName is the name of the plugin subcommand
// which must be implemented by plugins declaring support
// for hooks in their metadata.
HookSubcommandName = "docker-cli-plugin-hooks"
)
// Metadata provided by the plugin.
// Metadata provided by the plugin. See docs/extend/cli_plugins.md for canonical information.
type Metadata struct {
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
SchemaVersion string `json:",omitempty"`
@ -27,4 +22,7 @@ type Metadata struct {
ShortDescription string `json:",omitempty"`
// URL is a pointer to the plugin's homepage.
URL string `json:",omitempty"`
// Experimental specifies whether the plugin is experimental.
// Experimental plugins are not displayed on non-experimental CLIs.
Experimental bool `json:",omitempty"`
}

View File

@ -1,10 +1,7 @@
package manager
import (
"context"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
@ -13,7 +10,9 @@ import (
"github.com/spf13/cobra"
)
var pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
var (
pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
)
// Plugin represents a potential plugin with all it's metadata.
type Plugin struct {
@ -34,7 +33,9 @@ type Plugin struct {
// is set, and is always a `pluginError`, but the `Plugin` is still
// returned with no error. An error is only returned due to a
// non-recoverable error.
func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
//
// nolint: gocyclo
func newPlugin(c Candidate, rootcmd *cobra.Command, allowExperimental bool) (Plugin, error) {
path := c.Path()
if path == "" {
return Plugin{}, errors.New("plugin candidate path cannot be empty")
@ -65,20 +66,22 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
return p, nil
}
for _, cmd := range cmds {
// Ignore conflicts with commands which are
// just plugin stubs (i.e. from a previous
// call to AddPluginCommandStubs).
if IsPluginCommand(cmd) {
continue
}
if cmd.Name() == p.Name {
p.Err = NewPluginError("plugin %q duplicates builtin command", p.Name)
return p, nil
}
if cmd.HasAlias(p.Name) {
p.Err = NewPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name())
return p, nil
if rootcmd != nil {
for _, cmd := range rootcmd.Commands() {
// Ignore conflicts with commands which are
// just plugin stubs (i.e. from a previous
// call to AddPluginCommandStubs).
if p := cmd.Annotations[CommandAnnotationPlugin]; p == "true" {
continue
}
if cmd.Name() == p.Name {
p.Err = NewPluginError("plugin %q duplicates builtin command", p.Name)
return p, nil
}
if cmd.HasAlias(p.Name) {
p.Err = NewPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name())
return p, nil
}
}
}
@ -93,6 +96,10 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
p.Err = wrapAsPluginError(err, "invalid metadata")
return p, nil
}
if p.Experimental && !allowExperimental {
p.Err = &pluginError{errPluginRequireExperimental(p.Name)}
return p, nil
}
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
@ -103,22 +110,3 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
}
return p, nil
}
// RunHook executes the plugin's hooks command
// and returns its unprocessed output.
func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte, error) {
hDataBytes, err := json.Marshal(hookData)
if err != nil {
return nil, wrapAsPluginError(err, "failed to marshall hook data")
}
pCmd := exec.CommandContext(ctx, p.Path, p.Name, HookSubcommandName, string(hDataBytes))
pCmd.Env = os.Environ()
pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0])
hookCmdOutput, err := pCmd.Output()
if err != nil {
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")
}
return hookCmdOutput, nil
}

View File

@ -1,11 +1,10 @@
//go:build !windows
// +build !windows
package manager
func trimExeSuffix(s string) (string, error) {
return s, nil
}
func addExeSuffix(s string) string {
return s
}

View File

@ -1,7 +1,6 @@
package plugin
import (
"context"
"encoding/json"
"fmt"
"os"
@ -9,62 +8,34 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/socket"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/connhelper"
"github.com/docker/cli/cli/debug"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
)
// PersistentPreRunE must be called by any plugin command (or
// subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins
// which do not make use of `PersistentPreRun*` do not need to call
// this (although it remains safe to do so). Plugins are recommended
// to use `PersistentPreRunE` to enable the error to be
// to use `PersistenPreRunE` to enable the error to be
// returned. Should not be called outside of a command's
// PersistentPreRunE hook and must not be run unless Run has been
// called.
var PersistentPreRunE func(*cobra.Command, []string) error
// RunPlugin executes the specified plugin command
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) error {
func runPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) error {
tcmd := newPluginCommand(dockerCli, plugin, meta)
var persistentPreRunOnce sync.Once
PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
PersistentPreRunE = func(_ *cobra.Command, _ []string) error {
var err error
persistentPreRunOnce.Do(func() {
ctx, cancel := context.WithCancel(cmd.Context())
cmd.SetContext(ctx)
// Set up the context to cancel based on signalling via CLI socket.
socket.ConnectAndWait(cancel)
var opts []command.CLIOption
var opts []command.InitializeOpt
if os.Getenv("DOCKER_CLI_PLUGIN_USE_DIAL_STDIO") != "" {
opts = append(opts, withPluginClientConn(plugin.Name()))
}
opts = append(opts, command.WithEnableGlobalMeterProvider(), command.WithEnableGlobalTracerProvider())
err = tcmd.Initialize(opts...)
ogRunE := cmd.RunE
if ogRunE == nil {
ogRun := cmd.Run
// necessary because error will always be nil here
// see: https://github.com/golangci/golangci-lint/issues/1379
//nolint:unparam
ogRunE = func(cmd *cobra.Command, args []string) error {
ogRun(cmd, args)
return nil
}
cmd.Run = nil
}
cmd.RunE = func(cmd *cobra.Command, args []string) error {
stopInstrumentation := dockerCli.StartInstrumentation(cmd)
err := ogRunE(cmd, args)
stopInstrumentation(err)
return err
}
})
return err
}
@ -81,8 +52,6 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager
// 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 manager.Metadata) {
otel.SetErrorHandler(debug.OTELErrorHandler)
dockerCli, err := command.NewDockerCli()
if err != nil {
fmt.Fprintln(os.Stderr, err)
@ -91,7 +60,7 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
plugin := makeCmd(dockerCli)
if err := RunPlugin(dockerCli, plugin, meta); err != nil {
if err := runPlugin(dockerCli, plugin, meta); err != nil {
if sterr, ok := err.(cli.StatusError); ok {
if sterr.Status != "" {
fmt.Fprintln(dockerCli.Err(), sterr.Status)
@ -108,7 +77,7 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
}
}
func withPluginClientConn(name string) command.CLIOption {
func withPluginClientConn(name string) command.InitializeOpt {
return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) {
cmd := "docker"
if x := os.Getenv(manager.ReexecEnvvar); x != "" {
@ -155,17 +124,10 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
},
TraverseChildren: true,
DisableFlagsInUseLine: true,
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: false,
HiddenDefaultCmd: true,
DisableDescriptions: true,
},
}
opts, _ := cli.SetupPluginRootCommand(cmd)
opts, flags := cli.SetupPluginRootCommand(cmd)
cmd.SetIn(dockerCli.In())
cmd.SetOut(dockerCli.Out())
cmd.SetErr(dockerCli.Err())
cmd.SetOutput(dockerCli.Out())
cmd.AddCommand(
plugin,
@ -174,7 +136,7 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
cli.DisableFlagsInUseLine(cmd)
return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags())
return cli.NewTopLevelCommand(cmd, dockerCli, opts, flags)
}
func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
@ -197,11 +159,3 @@ func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.
}
return cmd
}
// RunningStandalone tells a CLI plugin it is run standalone by direct execution
func RunningStandalone() bool {
if os.Getenv(manager.ReexecEnvvar) != "" {
return false
}
return len(os.Args) < 2 || os.Args[1] != manager.MetadataSubcommandName
}

View File

@ -1,169 +0,0 @@
package socket
import (
"crypto/rand"
"encoding/hex"
"errors"
"io"
"net"
"os"
"runtime"
"sync"
"github.com/sirupsen/logrus"
)
// EnvKey represents the well-known environment variable used to pass the
// plugin being executed the socket name it should listen on to coordinate with
// the host CLI.
const EnvKey = "DOCKER_CLI_PLUGIN_SOCKET"
// NewPluginServer creates a plugin server that listens on a new Unix domain
// socket. h is called for each new connection to the socket in a goroutine.
func NewPluginServer(h func(net.Conn)) (*PluginServer, error) {
// Listen on a Unix socket, with the address being platform-dependent.
// When a non-abstract address is used, Go will unlink(2) the socket
// for us once the listener is closed, as documented in
// [net.UnixListener.SetUnlinkOnClose].
l, err := net.ListenUnix("unix", &net.UnixAddr{
Name: socketName("docker_cli_" + randomID()),
Net: "unix",
})
if err != nil {
return nil, err
}
logrus.Trace("Plugin server listening on ", l.Addr())
if h == nil {
h = func(net.Conn) {}
}
pl := &PluginServer{
l: l,
h: h,
}
go func() {
defer pl.Close()
for {
err := pl.accept()
if err != nil {
return
}
}
}()
return pl, nil
}
type PluginServer struct {
mu sync.Mutex
conns []net.Conn
l *net.UnixListener
h func(net.Conn)
closed bool
}
func (pl *PluginServer) accept() error {
conn, err := pl.l.Accept()
if err != nil {
return err
}
pl.mu.Lock()
defer pl.mu.Unlock()
if pl.closed {
// Handle potential race between Close and accept.
conn.Close()
return errors.New("plugin server is closed")
}
pl.conns = append(pl.conns, conn)
go pl.h(conn)
return nil
}
// Addr returns the [net.Addr] of the underlying [net.Listener].
func (pl *PluginServer) Addr() net.Addr {
return pl.l.Addr()
}
// Close ensures that the server is no longer accepting new connections and
// closes all existing connections. Existing connections will receive [io.EOF].
//
// The error value is that of the underlying [net.Listner.Close] call.
func (pl *PluginServer) Close() error {
if pl == nil {
return nil
}
logrus.Trace("Closing plugin server")
// Close connections first to ensure the connections get io.EOF instead
// of a connection reset.
pl.closeAllConns()
// Try to ensure that any active connections have a chance to receive
// io.EOF.
runtime.Gosched()
return pl.l.Close()
}
func (pl *PluginServer) closeAllConns() {
pl.mu.Lock()
defer pl.mu.Unlock()
if pl.closed {
return
}
// Prevent new connections from being accepted.
pl.closed = true
for _, conn := range pl.conns {
conn.Close()
}
pl.conns = nil
}
func randomID() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(err) // This shouldn't happen
}
return hex.EncodeToString(b)
}
// ConnectAndWait connects to the socket passed via well-known env var,
// if present, and attempts to read from it until it receives an EOF, at which
// point cb is called.
func ConnectAndWait(cb func()) {
socketAddr, ok := os.LookupEnv(EnvKey)
if !ok {
// if a plugin compiled against a more recent version of docker/cli
// is executed by an older CLI binary, ignore missing environment
// variable and behave as usual
return
}
addr, err := net.ResolveUnixAddr("unix", socketAddr)
if err != nil {
return
}
conn, err := net.DialUnix("unix", nil, addr)
if err != nil {
return
}
go func() {
b := make([]byte, 1)
for {
_, err := conn.Read(b)
if errors.Is(err, io.EOF) {
cb()
return
}
}
}()
}

View File

@ -1,9 +0,0 @@
//go:build windows || linux
package socket
func socketName(basename string) string {
// Address of an abstract socket -- this socket can be opened by name,
// but is not present in the filesystem.
return "@" + basename
}

View File

@ -1,14 +0,0 @@
//go:build !windows && !linux
package socket
import (
"os"
"path/filepath"
)
func socketName(basename string) string {
// Because abstract sockets are unavailable, use a socket path in the
// system temporary directory.
return filepath.Join(os.TempDir(), basename)
}

View File

@ -1,201 +0,0 @@
package socket
import (
"errors"
"io"
"io/fs"
"net"
"os"
"runtime"
"strings"
"sync/atomic"
"testing"
"time"
"gotest.tools/v3/assert"
"gotest.tools/v3/poll"
)
func TestPluginServer(t *testing.T) {
t.Run("connection closes with EOF when server closes", func(t *testing.T) {
called := make(chan struct{})
srv, err := NewPluginServer(func(_ net.Conn) { close(called) })
assert.NilError(t, err)
assert.Assert(t, srv != nil, "returned nil server but no error")
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String())
assert.NilError(t, err, "failed to resolve server address")
conn, err := net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to dial returned server")
defer conn.Close()
done := make(chan error, 1)
go func() {
_, err := conn.Read(make([]byte, 1))
done <- err
}()
select {
case <-called:
case <-time.After(10 * time.Millisecond):
t.Fatal("handler not called")
}
srv.Close()
select {
case err := <-done:
if !errors.Is(err, io.EOF) {
t.Fatalf("exepcted EOF error, got: %v", err)
}
case <-time.After(10 * time.Millisecond):
}
})
t.Run("allows reconnects", func(t *testing.T) {
var calls int32
h := func(_ net.Conn) {
atomic.AddInt32(&calls, 1)
}
srv, err := NewPluginServer(h)
assert.NilError(t, err)
defer srv.Close()
assert.Check(t, srv.Addr() != nil, "returned nil addr but no error")
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String())
assert.NilError(t, err, "failed to resolve server address")
waitForCalls := func(n int) {
poll.WaitOn(t, func(t poll.LogT) poll.Result {
if atomic.LoadInt32(&calls) == int32(n) {
return poll.Success()
}
return poll.Continue("waiting for handler to be called")
})
}
otherConn, err := net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to dial returned server")
otherConn.Close()
waitForCalls(1)
conn, err := net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to redial server")
defer conn.Close()
waitForCalls(2)
// and again but don't close the existing connection
conn2, err := net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to redial server")
defer conn2.Close()
waitForCalls(3)
srv.Close()
// now make sure we get EOF on the existing connections
buf := make([]byte, 1)
_, err = conn.Read(buf)
assert.ErrorIs(t, err, io.EOF, "expected EOF error, got: %v", err)
_, err = conn2.Read(buf)
assert.ErrorIs(t, err, io.EOF, "expected EOF error, got: %v", err)
})
t.Run("does not leak sockets to local directory", func(t *testing.T) {
srv, err := NewPluginServer(nil)
assert.NilError(t, err)
assert.Check(t, srv != nil, "returned nil server but no error")
checkDirNoNewPluginServer(t)
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String())
assert.NilError(t, err, "failed to resolve server address")
_, err = net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to dial returned server")
checkDirNoNewPluginServer(t)
})
t.Run("does not panic on Close if server is nil", func(t *testing.T) {
var srv *PluginServer
defer func() {
if r := recover(); r != nil {
t.Errorf("panicked on Close")
}
}()
err := srv.Close()
assert.NilError(t, err)
})
}
func checkDirNoNewPluginServer(t *testing.T) {
t.Helper()
files, err := os.ReadDir(".")
assert.NilError(t, err, "failed to list files in dir to check for leaked sockets")
for _, f := range files {
info, err := f.Info()
assert.NilError(t, err, "failed to check file info")
// check for a socket with `docker_cli_` in the name (from `SetupConn()`)
if strings.Contains(f.Name(), "docker_cli_") && info.Mode().Type() == fs.ModeSocket {
t.Fatal("found socket in a local directory")
}
}
}
func TestConnectAndWait(t *testing.T) {
t.Run("calls cancel func on EOF", func(t *testing.T) {
srv, err := NewPluginServer(nil)
assert.NilError(t, err, "failed to setup server")
defer srv.Close()
done := make(chan struct{})
t.Setenv(EnvKey, srv.Addr().String())
cancelFunc := func() {
done <- struct{}{}
}
ConnectAndWait(cancelFunc)
select {
case <-done:
t.Fatal("unexpectedly done")
default:
}
srv.Close()
select {
case <-done:
case <-time.After(10 * time.Millisecond):
t.Fatal("cancel function not closed after 10ms")
}
})
// TODO: this test cannot be executed with `t.Parallel()`, due to
// relying on goroutine numbers to ensure correct behaviour
t.Run("connect goroutine exits after EOF", func(t *testing.T) {
srv, err := NewPluginServer(nil)
assert.NilError(t, err, "failed to setup server")
defer srv.Close()
t.Setenv(EnvKey, srv.Addr().String())
numGoroutines := runtime.NumGoroutine()
ConnectAndWait(func() {})
assert.Equal(t, runtime.NumGoroutine(), numGoroutines+1)
srv.Close()
poll.WaitOn(t, func(t poll.LogT) poll.Result {
if runtime.NumGoroutine() > numGoroutines+1 {
return poll.Continue("waiting for connect goroutine to exit")
}
return poll.Success()
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(10*time.Millisecond))
})
}

View File

@ -3,18 +3,13 @@ package cli
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
pluginmanager "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/command"
cliconfig "github.com/docker/cli/cli/config"
cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/docker/pkg/homedir"
"github.com/docker/docker/registry"
"github.com/fvbommel/sortorder"
"github.com/moby/term"
"github.com/morikuni/aec"
"github.com/docker/docker/pkg/term"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@ -22,30 +17,24 @@ import (
// setupCommonRootCommand contains the setup common to
// SetupRootCommand and SetupPluginRootCommand.
func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *cobra.Command) {
func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet, *cobra.Command) {
opts := cliflags.NewClientOptions()
opts.InstallFlags(rootCmd.Flags())
flags := rootCmd.Flags()
flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files")
opts.Common.InstallFlags(flags)
cobra.AddTemplateFunc("add", func(a, b int) int { return a + b })
cobra.AddTemplateFunc("hasAliases", hasAliases)
cobra.AddTemplateFunc("hasSubCommands", hasSubCommands)
cobra.AddTemplateFunc("hasTopCommands", hasTopCommands)
cobra.AddTemplateFunc("hasManagementSubCommands", hasManagementSubCommands)
cobra.AddTemplateFunc("hasSwarmSubCommands", hasSwarmSubCommands)
cobra.AddTemplateFunc("hasInvalidPlugins", hasInvalidPlugins)
cobra.AddTemplateFunc("topCommands", topCommands)
cobra.AddTemplateFunc("commandAliases", commandAliases)
cobra.AddTemplateFunc("operationSubCommands", operationSubCommands)
cobra.AddTemplateFunc("managementSubCommands", managementSubCommands)
cobra.AddTemplateFunc("orchestratorSubCommands", orchestratorSubCommands)
cobra.AddTemplateFunc("invalidPlugins", invalidPlugins)
cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages)
cobra.AddTemplateFunc("vendorAndVersion", vendorAndVersion)
cobra.AddTemplateFunc("invalidPluginReason", invalidPluginReason)
cobra.AddTemplateFunc("isPlugin", isPlugin)
cobra.AddTemplateFunc("isExperimental", isExperimental)
cobra.AddTemplateFunc("hasAdditionalHelp", hasAdditionalHelp)
cobra.AddTemplateFunc("additionalHelp", additionalHelp)
cobra.AddTemplateFunc("decoratedName", decoratedName)
rootCmd.SetUsageTemplate(usageTemplate)
@ -54,35 +43,26 @@ func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *c
rootCmd.SetHelpCommand(helpCommand)
rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage")
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "use --help")
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")
rootCmd.PersistentFlags().Lookup("help").Hidden = true
rootCmd.Annotations = map[string]string{
"additionalHelp": "For more help on how to use Docker, head to https://docs.docker.com/go/guides/",
"docs.code-delimiter": `"`, // https://github.com/docker/cli-docs-tool/blob/77abede22166eaea4af7335096bdcedd043f5b19/annotation/annotation.go#L20-L22
}
// Configure registry.CertsDir() when running in rootless-mode
if os.Getenv("ROOTLESSKIT_STATE_DIR") != "" {
if configHome, err := homedir.GetConfigHome(); err == nil {
registry.SetCertsDir(filepath.Join(configHome, "docker/certs.d"))
}
}
return opts, helpCommand
return opts, flags, helpCommand
}
// SetupRootCommand sets default usage, help, and error handling for the
// root command.
func SetupRootCommand(rootCmd *cobra.Command) (opts *cliflags.ClientOptions, helpCmd *cobra.Command) {
func SetupRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet, *cobra.Command) {
opts, flags, helpCmd := setupCommonRootCommand(rootCmd)
rootCmd.SetVersionTemplate("Docker version {{.Version}}\n")
return setupCommonRootCommand(rootCmd)
return opts, flags, helpCmd
}
// SetupPluginRootCommand sets default usage, help and error handling for a plugin root command.
func SetupPluginRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) {
opts, _ := setupCommonRootCommand(rootCmd)
return opts, rootCmd.Flags()
opts, flags, _ := setupCommonRootCommand(rootCmd)
return opts, flags
}
// FlagErrorFunc prints an error message which matches the format of the
@ -115,13 +95,7 @@ type TopLevelCommand struct {
// NewTopLevelCommand returns a new TopLevelCommand object
func NewTopLevelCommand(cmd *cobra.Command, dockerCli *command.DockerCli, opts *cliflags.ClientOptions, flags *pflag.FlagSet) *TopLevelCommand {
return &TopLevelCommand{
cmd: cmd,
dockerCli: dockerCli,
opts: opts,
flags: flags,
args: os.Args[1:],
}
return &TopLevelCommand{cmd, dockerCli, opts, flags, os.Args[1:]}
}
// SetArgs sets the args (default os.Args[:1] used to invoke the command
@ -176,8 +150,8 @@ func (tcmd *TopLevelCommand) HandleGlobalFlags() (*cobra.Command, []string, erro
}
// Initialize finalises global option parsing and initializes the docker client.
func (tcmd *TopLevelCommand) Initialize(ops ...command.CLIOption) error {
tcmd.opts.SetDefaultOptions(tcmd.flags)
func (tcmd *TopLevelCommand) Initialize(ops ...command.InitializeOpt) error {
tcmd.opts.Common.SetDefaultOptions(tcmd.flags)
return tcmd.dockerCli.Initialize(tcmd.opts, ops...)
}
@ -200,16 +174,6 @@ func DisableFlagsInUseLine(cmd *cobra.Command) {
})
}
// HasCompletionArg returns true if a cobra completion arg request is found.
func HasCompletionArg(args []string) bool {
for _, arg := range args {
if arg == cobra.ShellCompRequestCmd || arg == cobra.ShellCompNoDescRequestCmd {
return true
}
}
return false
}
var helpCommand = &cobra.Command{
Use: "help [command]",
Short: "Help about the command",
@ -220,47 +184,15 @@ var helpCommand = &cobra.Command{
if cmd == nil || e != nil || len(args) > 0 {
return errors.Errorf("unknown help topic: %v", strings.Join(args, " "))
}
helpFunc := cmd.HelpFunc()
helpFunc(cmd, args)
return nil
},
}
func isExperimental(cmd *cobra.Command) bool {
if _, ok := cmd.Annotations["experimentalCLI"]; ok {
return true
}
var experimental bool
cmd.VisitParents(func(cmd *cobra.Command) {
if _, ok := cmd.Annotations["experimentalCLI"]; ok {
experimental = true
}
})
return experimental
}
func additionalHelp(cmd *cobra.Command) string {
if msg, ok := cmd.Annotations["additionalHelp"]; ok {
out := cmd.OutOrStderr()
if _, isTerminal := term.GetFdInfo(out); !isTerminal {
return msg
}
style := aec.EmptyBuilder.Bold().ANSI
return style.Apply(msg)
}
return ""
}
func hasAdditionalHelp(cmd *cobra.Command) bool {
return additionalHelp(cmd) != ""
}
func isPlugin(cmd *cobra.Command) bool {
return pluginmanager.IsPluginCommand(cmd)
}
func hasAliases(cmd *cobra.Command) bool {
return len(cmd.Aliases) > 0 || cmd.Annotations["aliases"] != ""
return cmd.Annotations[pluginmanager.CommandAnnotationPlugin] == "true"
}
func hasSubCommands(cmd *cobra.Command) bool {
@ -271,69 +203,16 @@ func hasManagementSubCommands(cmd *cobra.Command) bool {
return len(managementSubCommands(cmd)) > 0
}
func hasSwarmSubCommands(cmd *cobra.Command) bool {
return len(orchestratorSubCommands(cmd)) > 0
}
func hasInvalidPlugins(cmd *cobra.Command) bool {
return len(invalidPlugins(cmd)) > 0
}
func hasTopCommands(cmd *cobra.Command) bool {
return len(topCommands(cmd)) > 0
}
// commandAliases is a templating function to return aliases for the command,
// formatted as the full command as they're called (contrary to the default
// Aliases function, which only returns the subcommand).
func commandAliases(cmd *cobra.Command) string {
if cmd.Annotations["aliases"] != "" {
return cmd.Annotations["aliases"]
}
var parentPath string
if cmd.HasParent() {
parentPath = cmd.Parent().CommandPath() + " "
}
aliases := cmd.CommandPath()
for _, alias := range cmd.Aliases {
aliases += ", " + parentPath + alias
}
return aliases
}
func topCommands(cmd *cobra.Command) []*cobra.Command {
cmds := []*cobra.Command{}
if cmd.Parent() != nil {
// for now, only use top-commands for the root-command, and skip
// for sub-commands
return cmds
}
for _, sub := range cmd.Commands() {
if isPlugin(sub) || !sub.IsAvailableCommand() {
continue
}
if _, ok := sub.Annotations["category-top"]; ok {
cmds = append(cmds, sub)
}
}
sort.SliceStable(cmds, func(i, j int) bool {
return sortorder.NaturalLess(cmds[i].Annotations["category-top"], cmds[j].Annotations["category-top"])
})
return cmds
}
func operationSubCommands(cmd *cobra.Command) []*cobra.Command {
cmds := []*cobra.Command{}
for _, sub := range cmd.Commands() {
if isPlugin(sub) {
continue
}
if _, ok := sub.Annotations["category-top"]; ok {
if cmd.Parent() == nil {
// for now, only use top-commands for the root-command
continue
}
}
if sub.IsAvailableCommand() && !sub.HasSubCommands() {
cmds = append(cmds, sub)
}
@ -369,27 +248,6 @@ func vendorAndVersion(cmd *cobra.Command) string {
}
func managementSubCommands(cmd *cobra.Command) []*cobra.Command {
cmds := []*cobra.Command{}
for _, sub := range allManagementSubCommands(cmd) {
if _, ok := sub.Annotations["swarm"]; ok {
continue
}
cmds = append(cmds, sub)
}
return cmds
}
func orchestratorSubCommands(cmd *cobra.Command) []*cobra.Command {
cmds := []*cobra.Command{}
for _, sub := range allManagementSubCommands(cmd) {
if _, ok := sub.Annotations["swarm"]; ok {
cmds = append(cmds, sub)
}
}
return cmds
}
func allManagementSubCommands(cmd *cobra.Command) []*cobra.Command {
cmds := []*cobra.Command{}
for _, sub := range cmd.Commands() {
if isPlugin(sub) {
@ -422,26 +280,17 @@ func invalidPluginReason(cmd *cobra.Command) string {
return cmd.Annotations[pluginmanager.CommandAnnotationPluginInvalid]
}
const usageTemplate = `Usage:
var usageTemplate = `Usage:
{{- if not .HasSubCommands}} {{.UseLine}}{{end}}
{{- if .HasSubCommands}} {{ .CommandPath}}{{- if .HasAvailableFlags}} [OPTIONS]{{end}} COMMAND{{end}}
{{- if not .HasSubCommands}} {{.UseLine}}{{end}}
{{- if .HasSubCommands}} {{ .CommandPath}}{{- if .HasAvailableFlags}} [OPTIONS]{{end}} COMMAND{{end}}
{{if ne .Long ""}}{{ .Long | trim }}{{ else }}{{ .Short | trim }}{{end}}
{{- if isExperimental .}}
EXPERIMENTAL:
{{.CommandPath}} is an experimental feature.
Experimental features provide early access to product functionality. These
features may change between releases without warning, or can be removed from a
future release. Learn more about experimental features in our documentation:
https://docs.docker.com/go/experimental/
{{- end}}
{{- if hasAliases . }}
{{- if gt .Aliases 0}}
Aliases:
{{ commandAliases . }}
{{.NameAndAliases}}
{{- end}}
{{- if .HasExample}}
@ -450,36 +299,18 @@ Examples:
{{ .Example }}
{{- end}}
{{- if .HasParent}}
{{- if .HasAvailableFlags}}
Options:
{{ wrappedFlagUsages . | trimRightSpace}}
{{- end}}
{{- end}}
{{- if hasTopCommands .}}
Common Commands:
{{- range topCommands .}}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
{{- end}}
{{- end}}
{{- if hasManagementSubCommands . }}
Management Commands:
{{- range managementSubCommands . }}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
{{- end}}
{{- end}}
{{- if hasSwarmSubCommands . }}
Swarm Commands:
{{- range orchestratorSubCommands . }}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
{{- end}}
{{- end}}
@ -500,26 +331,13 @@ Invalid Plugins:
{{rpad .Name .NamePadding }} {{invalidPluginReason .}}
{{- end}}
{{- end}}
{{- if not .HasParent}}
{{- if .HasAvailableFlags}}
Global Options:
{{ wrappedFlagUsages . | trimRightSpace}}
{{- end}}
{{- end}}
{{- if .HasSubCommands }}
Run '{{.CommandPath}} COMMAND --help' for more information on a command.
{{- end}}
{{- if hasAdditionalHelp .}}
{{ additionalHelp . }}
{{- end}}
`
const helpTemplate = `
var helpTemplate = `
{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`

View File

@ -6,8 +6,8 @@ import (
pluginmanager "github.com/docker/cli/cli-plugins/manager"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestVisitAll(t *testing.T) {
@ -78,23 +78,6 @@ func TestInvalidPlugin(t *testing.T) {
assert.DeepEqual(t, invalidPlugins(root), []*cobra.Command{sub1}, cmpopts.IgnoreUnexported(cobra.Command{}))
}
func TestCommandAliases(t *testing.T) {
root := &cobra.Command{Use: "root"}
sub := &cobra.Command{Use: "subcommand", Aliases: []string{"alias1", "alias2"}}
sub2 := &cobra.Command{Use: "subcommand2", Annotations: map[string]string{"aliases": "root foo, root bar"}}
root.AddCommand(sub)
root.AddCommand(sub2)
assert.Equal(t, hasAliases(sub), true)
assert.Equal(t, commandAliases(sub), "root subcommand, root alias1, root alias2")
assert.Equal(t, hasAliases(sub2), true)
assert.Equal(t, commandAliases(sub2), "root foo, root bar")
sub.Annotations = map[string]string{"aliases": "custom alias, custom alias 2"}
assert.Equal(t, hasAliases(sub), true)
assert.Equal(t, commandAliases(sub), "custom alias, custom alias 2")
}
func TestDecoratedName(t *testing.T) {
root := &cobra.Command{Use: "root"}
topLevelCommand := &cobra.Command{Use: "pluginTopLevelCommand"}

View File

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

View File

@ -2,16 +2,13 @@ package builder
import (
"context"
"errors"
"fmt"
"strings"
"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/docker/docker/api/types"
"github.com/docker/docker/errdefs"
units "github.com/docker/go-units"
"github.com/spf13/cobra"
)
@ -32,7 +29,7 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
Short: "Remove build cache",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCli, options)
spaceReclaimed, output, err := runPrune(dockerCli, options)
if err != nil {
return err
}
@ -42,14 +39,13 @@ 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: completion.NoComplete,
Annotations: map[string]string{"version": "1.39"},
}
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.BoolVarP(&options.all, "all", "a", false, "Remove all unused images, not just dangling ones")
flags.Var(&options.filter, "filter", "Provide filter values (e.g. 'unused-for=24h')")
flags.Var(&options.keepStorage, "keep-storage", "Amount of disk space to keep for cache")
return cmd
@ -60,7 +56,7 @@ const (
allCacheWarning = `WARNING! This will remove all build cache. Are you sure you want to continue?`
)
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
pruneFilters := options.filter.Value()
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
@ -68,17 +64,11 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
if options.all {
warning = allCacheWarning
}
if !options.force {
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
if err != nil {
return 0, "", err
}
if !r {
return 0, "", errdefs.Cancelled(errors.New("builder prune has been cancelled"))
}
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
return 0, "", nil
}
report, err := dockerCli.Client().BuildCachePrune(ctx, types.BuildCachePruneOptions{
report, err := dockerCli.Client().BuildCachePrune(context.Background(), types.BuildCachePruneOptions{
All: options.all,
KeepStorage: options.keepStorage.Value(),
Filters: pruneFilters,
@ -101,6 +91,6 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
}
// 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})
func CachePrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter})
}

View File

@ -1,26 +0,0 @@
package builder
import (
"context"
"errors"
"io"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
)
func TestBuilderPromptTermination(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
builderPruneFunc: func(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
return nil, errors.New("fakeClient builderPruneFunc should not be called")
},
})
cmd := NewPruneCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
test.TerminatePrompt(ctx, t, cmd, cli)
}

View File

@ -0,0 +1,70 @@
package bundlefile
import (
"encoding/json"
"io"
"github.com/pkg/errors"
)
// Bundlefile stores the contents of a bundlefile
type Bundlefile struct {
Version string
Services map[string]Service
}
// Service is a service from a bundlefile
type Service struct {
Image string
Command []string `json:",omitempty"`
Args []string `json:",omitempty"`
Env []string `json:",omitempty"`
Labels map[string]string `json:",omitempty"`
Ports []Port `json:",omitempty"`
WorkingDir *string `json:",omitempty"`
User *string `json:",omitempty"`
Networks []string `json:",omitempty"`
}
// Port is a port as defined in a bundlefile
type Port struct {
Protocol string
Port uint32
}
// LoadFile loads a bundlefile from a path to the file
func LoadFile(reader io.Reader) (*Bundlefile, error) {
bundlefile := &Bundlefile{}
decoder := json.NewDecoder(reader)
if err := decoder.Decode(bundlefile); err != nil {
switch jsonErr := err.(type) {
case *json.SyntaxError:
return nil, errors.Errorf(
"JSON syntax error at byte %v: %s",
jsonErr.Offset,
jsonErr.Error())
case *json.UnmarshalTypeError:
return nil, errors.Errorf(
"Unexpected type at byte %v. Expected %s but received %s.",
jsonErr.Offset,
jsonErr.Type,
jsonErr.Value)
}
return nil, err
}
return bundlefile, nil
}
// Print writes the contents of the bundlefile to the output writer
// as human readable json
func Print(out io.Writer, bundle *Bundlefile) error {
bytes, err := json.MarshalIndent(*bundle, "", " ")
if err != nil {
return err
}
_, err = out.Write(bytes)
return err
}

View File

@ -0,0 +1,78 @@
package bundlefile
import (
"bytes"
"strings"
"testing"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestLoadFileV01Success(t *testing.T) {
reader := strings.NewReader(`{
"Version": "0.1",
"Services": {
"redis": {
"Image": "redis@sha256:4b24131101fa0117bcaa18ac37055fffd9176aa1a240392bb8ea85e0be50f2ce",
"Networks": ["default"]
},
"web": {
"Image": "dockercloud/hello-world@sha256:fe79a2cfbd17eefc344fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d",
"Networks": ["default"],
"User": "web"
}
}
}`)
bundle, err := LoadFile(reader)
assert.NilError(t, err)
assert.Check(t, is.Equal("0.1", bundle.Version))
assert.Check(t, is.Len(bundle.Services, 2))
}
func TestLoadFileSyntaxError(t *testing.T) {
reader := strings.NewReader(`{
"Version": "0.1",
"Services": unquoted string
}`)
_, err := LoadFile(reader)
assert.Error(t, err, "JSON syntax error at byte 37: invalid character 'u' looking for beginning of value")
}
func TestLoadFileTypeError(t *testing.T) {
reader := strings.NewReader(`{
"Version": "0.1",
"Services": {
"web": {
"Image": "redis",
"Networks": "none"
}
}
}`)
_, err := LoadFile(reader)
assert.Error(t, err, "Unexpected type at byte 94. Expected []string but received string.")
}
func TestPrint(t *testing.T) {
var buffer bytes.Buffer
bundle := &Bundlefile{
Version: "0.1",
Services: map[string]Service{
"web": {
Image: "image",
Command: []string{"echo", "something"},
},
},
}
assert.Check(t, Print(&buffer, bundle))
output := buffer.String()
assert.Check(t, is.Contains(output, "\"Image\": \"image\""))
assert.Check(t, is.Contains(output,
`"Command": [
"echo",
"something"
]`))
}

View File

@ -3,34 +3,34 @@ package checkpoint
import (
"context"
"github.com/docker/docker/api/types/checkpoint"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
type fakeClient struct {
client.Client
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)
checkpointCreateFunc func(container string, options types.CheckpointCreateOptions) error
checkpointDeleteFunc func(container string, options types.CheckpointDeleteOptions) error
checkpointListFunc func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error)
}
func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options checkpoint.CreateOptions) error {
func (cli *fakeClient) CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error {
if cli.checkpointCreateFunc != nil {
return cli.checkpointCreateFunc(container, options)
}
return nil
}
func (cli *fakeClient) CheckpointDelete(_ context.Context, container string, options checkpoint.DeleteOptions) error {
func (cli *fakeClient) CheckpointDelete(ctx context.Context, container string, options types.CheckpointDeleteOptions) error {
if cli.checkpointDeleteFunc != nil {
return cli.checkpointDeleteFunc(container, options)
}
return nil
}
func (cli *fakeClient) CheckpointList(_ context.Context, container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
func (cli *fakeClient) CheckpointList(ctx context.Context, container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
if cli.checkpointListFunc != nil {
return cli.checkpointListFunc(container, options)
}
return []checkpoint.Summary{}, nil
return []types.Checkpoint{}, nil
}

View File

@ -6,8 +6,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/docker/api/types/checkpoint"
"github.com/docker/docker/api/types"
"github.com/spf13/cobra"
)
@ -28,24 +27,27 @@ 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(dockerCli, opts)
},
ValidArgsFunction: completion.NoComplete,
}
flags := cmd.Flags()
flags.BoolVar(&opts.leaveRunning, "leave-running", false, "Leave the container running after checkpoint")
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
return cmd
}
func runCreate(ctx context.Context, dockerCli command.Cli, opts createOptions) error {
err := dockerCli.Client().CheckpointCreate(ctx, opts.container, checkpoint.CreateOptions{
func runCreate(dockerCli command.Cli, opts createOptions) error {
client := dockerCli.Client()
checkpointOpts := types.CheckpointCreateOptions{
CheckpointID: opts.checkpoint,
CheckpointDir: opts.checkpointDir,
Exit: !opts.leaveRunning,
})
}
err := client.CheckpointCreate(context.Background(), opts.container, checkpointOpts)
if err != nil {
return err
}

View File

@ -1,21 +1,21 @@
package checkpoint
import (
"io"
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/checkpoint"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestCheckpointCreateErrors(t *testing.T) {
testCases := []struct {
args []string
checkpointCreateFunc func(container string, options checkpoint.CreateOptions) error
checkpointCreateFunc func(container string, options types.CheckpointCreateOptions) error
expectedError string
}{
{
@ -28,7 +28,7 @@ func TestCheckpointCreateErrors(t *testing.T) {
},
{
args: []string{"foo", "bar"},
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
checkpointCreateFunc: func(container string, options types.CheckpointCreateOptions) error {
return errors.Errorf("error creating checkpoint for container foo")
},
expectedError: "error creating checkpoint for container foo",
@ -41,8 +41,7 @@ func TestCheckpointCreateErrors(t *testing.T) {
})
cmd := newCreateCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
@ -51,7 +50,7 @@ func TestCheckpointCreateWithOptions(t *testing.T) {
var containerID, checkpointID, checkpointDir string
var exit bool
cli := test.NewFakeCli(&fakeClient{
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
checkpointCreateFunc: func(container string, options types.CheckpointCreateOptions) error {
containerID = container
checkpointID = options.CheckpointID
checkpointDir = options.CheckpointDir
@ -60,14 +59,14 @@ func TestCheckpointCreateWithOptions(t *testing.T) {
},
})
cmd := newCreateCommand(cli)
cp := "checkpoint-bar"
cmd.SetArgs([]string{"container-foo", cp})
checkpoint := "checkpoint-bar"
cmd.SetArgs([]string{"container-foo", checkpoint})
cmd.Flags().Set("leave-running", "true")
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(cp, checkpointID))
assert.Check(t, is.Equal(checkpoint, checkpointID))
assert.Check(t, is.Equal("/dir/foo", checkpointDir))
assert.Check(t, is.Equal(false, exit))
assert.Check(t, is.Equal(cp, strings.TrimSpace(cli.OutBuffer().String())))
assert.Check(t, is.Equal(checkpoint, strings.TrimSpace(cli.OutBuffer().String())))
}

View File

@ -2,27 +2,29 @@ package checkpoint
import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/checkpoint"
"github.com/docker/docker/api/types"
)
const (
defaultCheckpointFormat = "table {{.Name}}"
checkpointNameHeader = "CHECKPOINT NAME"
checkpointNameHeader = "CHECKPOINT NAME"
)
// NewFormat returns a format for use with a checkpoint Context
func NewFormat(source string) formatter.Format {
if source == formatter.TableFormatKey {
switch source {
case formatter.TableFormatKey:
return defaultCheckpointFormat
}
return formatter.Format(source)
}
// FormatWrite writes formatted checkpoints using the Context
func FormatWrite(ctx formatter.Context, checkpoints []checkpoint.Summary) error {
func FormatWrite(ctx formatter.Context, checkpoints []types.Checkpoint) error {
render := func(format func(subContext formatter.SubContext) error) error {
for _, cp := range checkpoints {
if err := format(&checkpointContext{c: cp}); err != nil {
for _, checkpoint := range checkpoints {
if err := format(&checkpointContext{c: checkpoint}); err != nil {
return err
}
}
@ -33,7 +35,7 @@ func FormatWrite(ctx formatter.Context, checkpoints []checkpoint.Summary) error
type checkpointContext struct {
formatter.HeaderContext
c checkpoint.Summary
c types.Checkpoint
}
func newCheckpointContext() *checkpointContext {

View File

@ -5,8 +5,8 @@ import (
"testing"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/checkpoint"
"gotest.tools/v3/assert"
"github.com/docker/docker/api/types"
"gotest.tools/assert"
)
func TestCheckpointContextFormatWrite(t *testing.T) {
@ -38,14 +38,15 @@ checkpoint-3:
},
}
checkpoints := []types.Checkpoint{
{Name: "checkpoint-1"},
{Name: "checkpoint-2"},
{Name: "checkpoint-3"},
}
for _, testcase := range cases {
out := bytes.NewBufferString("")
testcase.context.Output = out
err := FormatWrite(testcase.context, []checkpoint.Summary{
{Name: "checkpoint-1"},
{Name: "checkpoint-2"},
{Name: "checkpoint-3"},
})
err := FormatWrite(testcase.context, checkpoints)
assert.NilError(t, err)
assert.Equal(t, out.String(), testcase.expected)
}

View File

@ -5,9 +5,8 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/checkpoint"
"github.com/docker/docker/api/types"
"github.com/spf13/cobra"
)
@ -24,21 +23,25 @@ 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(dockerCli, args[0], opts)
},
ValidArgsFunction: completion.ContainerNames(dockerCli, false),
}
flags := cmd.Flags()
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
return cmd
}
func runList(ctx context.Context, dockerCli command.Cli, container string, opts listOptions) error {
checkpoints, err := dockerCli.Client().CheckpointList(ctx, container, checkpoint.ListOptions{
func runList(dockerCli command.Cli, container string, opts listOptions) error {
client := dockerCli.Client()
listOpts := types.CheckpointListOptions{
CheckpointDir: opts.checkpointDir,
})
}
checkpoints, err := client.CheckpointList(context.Background(), container, listOpts)
if err != nil {
return err
}

View File

@ -1,21 +1,21 @@
package checkpoint
import (
"io"
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/checkpoint"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/golden"
)
func TestCheckpointListErrors(t *testing.T) {
testCases := []struct {
args []string
checkpointListFunc func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error)
checkpointListFunc func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error)
expectedError string
}{
{
@ -28,8 +28,8 @@ func TestCheckpointListErrors(t *testing.T) {
},
{
args: []string{"foo"},
checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
return []checkpoint.Summary{}, errors.Errorf("error getting checkpoints for container foo")
checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
return []types.Checkpoint{}, errors.Errorf("error getting checkpoints for container foo")
},
expectedError: "error getting checkpoints for container foo",
},
@ -41,8 +41,7 @@ func TestCheckpointListErrors(t *testing.T) {
})
cmd := newListCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
@ -50,10 +49,10 @@ func TestCheckpointListErrors(t *testing.T) {
func TestCheckpointListWithOptions(t *testing.T) {
var containerID, checkpointDir string
cli := test.NewFakeCli(&fakeClient{
checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
containerID = container
checkpointDir = options.CheckpointDir
return []checkpoint.Summary{
return []types.Checkpoint{
{Name: "checkpoint-foo"},
}, nil
},

View File

@ -5,7 +5,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types/checkpoint"
"github.com/docker/docker/api/types"
"github.com/spf13/cobra"
)
@ -22,19 +22,23 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
Short: "Remove a checkpoint",
Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
return runRemove(cmd.Context(), dockerCli, args[0], args[1], opts)
return runRemove(dockerCli, args[0], args[1], opts)
},
}
flags := cmd.Flags()
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
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,
func runRemove(dockerCli command.Cli, container string, checkpoint string, opts removeOptions) error {
client := dockerCli.Client()
removeOpts := types.CheckpointDeleteOptions{
CheckpointID: checkpoint,
CheckpointDir: opts.checkpointDir,
})
}
return client.CheckpointDelete(context.Background(), container, removeOpts)
}

View File

@ -1,20 +1,20 @@
package checkpoint
import (
"io"
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/checkpoint"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestCheckpointRemoveErrors(t *testing.T) {
testCases := []struct {
args []string
checkpointDeleteFunc func(container string, options checkpoint.DeleteOptions) error
checkpointDeleteFunc func(container string, options types.CheckpointDeleteOptions) error
expectedError string
}{
{
@ -27,7 +27,7 @@ func TestCheckpointRemoveErrors(t *testing.T) {
},
{
args: []string{"foo", "bar"},
checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error {
return errors.Errorf("error deleting checkpoint")
},
expectedError: "error deleting checkpoint",
@ -40,8 +40,7 @@ func TestCheckpointRemoveErrors(t *testing.T) {
})
cmd := newRemoveCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
@ -49,7 +48,7 @@ func TestCheckpointRemoveErrors(t *testing.T) {
func TestCheckpointRemoveWithOptions(t *testing.T) {
var containerID, checkpointID, checkpointDir string
cli := test.NewFakeCli(&fakeClient{
checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error {
containerID = container
checkpointID = options.CheckpointID
checkpointDir = options.CheckpointDir

View File

@ -1,20 +1,16 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.21
package command
import (
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strconv"
"sync"
"time"
"github.com/docker/cli/cli/config"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
dcontext "github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/docker"
@ -26,97 +22,76 @@ import (
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/cli/version"
"github.com/docker/cli/internal/containerizedengine"
dopts "github.com/docker/cli/opts"
"github.com/docker/docker/api"
clitypes "github.com/docker/cli/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/swarm"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/term"
"github.com/docker/go-connections/tlsconfig"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/theupdateframework/notary"
notaryclient "github.com/theupdateframework/notary/client"
"github.com/theupdateframework/notary/passphrase"
)
const defaultInitTimeout = 2 * time.Second
// Streams is an interface which exposes the standard input and output streams
type Streams interface {
In() *streams.In
Out() *streams.Out
Err() *streams.Out
Err() io.Writer
}
// Cli represents the docker command line client.
type Cli interface {
Client() client.APIClient
Streams
Out() *streams.Out
Err() io.Writer
In() *streams.In
SetIn(in *streams.In)
Apply(ops ...CLIOption) error
Apply(ops ...DockerCliOption) error
ConfigFile() *configfile.ConfigFile
ServerInfo() ServerInfo
ClientInfo() ClientInfo
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
DefaultVersion() string
CurrentVersion() string
ManifestStore() manifeststore.Store
RegistryClient(bool) registryclient.RegistryClient
ContentTrustEnabled() bool
BuildKitEnabled() (bool, error)
NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error)
ContextStore() store.Store
CurrentContext() string
StackOrchestrator(flagValue string) (Orchestrator, error)
DockerEndpoint() docker.Endpoint
TelemetryClient
}
// DockerCli is an instance the docker command line client.
// Instances of the client can be returned from NewDockerCli.
type DockerCli struct {
configFile *configfile.ConfigFile
options *cliflags.ClientOptions
in *streams.In
out *streams.Out
err *streams.Out
client client.APIClient
serverInfo ServerInfo
contentTrust bool
contextStore store.Store
currentContext string
init sync.Once
initErr error
dockerEndpoint docker.Endpoint
contextStoreConfig store.Config
initTimeout time.Duration
res telemetryResource
// baseCtx is the base context used for internal operations. In the future
// this may be replaced by explicitly passing a context to functions that
// need it.
baseCtx context.Context
enableGlobalMeter, enableGlobalTracer bool
configFile *configfile.ConfigFile
in *streams.In
out *streams.Out
err io.Writer
client client.APIClient
serverInfo ServerInfo
clientInfo ClientInfo
contentTrust bool
newContainerizeClient func(string) (clitypes.ContainerizedClient, error)
contextStore store.Store
currentContext string
dockerEndpoint docker.Endpoint
contextStoreConfig store.Config
}
// DefaultVersion returns api.defaultVersion.
// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
func (cli *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 api.DefaultVersion
}
return cli.client.ClientVersion()
return cli.clientInfo.DefaultVersion
}
// Client returns the APIClient
func (cli *DockerCli) Client() client.APIClient {
if err := cli.initialize(); err != nil {
_, _ = fmt.Fprintf(cli.Err(), "Failed to initialize: %s\n", err)
os.Exit(1)
}
return cli.client
}
@ -126,7 +101,7 @@ func (cli *DockerCli) Out() *streams.Out {
}
// Err returns the writer used for stderr
func (cli *DockerCli) Err() *streams.Out {
func (cli *DockerCli) Err() io.Writer {
return cli.err
}
@ -143,7 +118,7 @@ func (cli *DockerCli) In() *streams.In {
// ShowHelp shows the command help.
func ShowHelp(err io.Writer) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
cmd.SetOut(err)
cmd.SetOutput(err)
cmd.HelpFunc()(cmd, args)
return nil
}
@ -151,83 +126,38 @@ func ShowHelp(err io.Writer) func(*cobra.Command, []string) error {
// ConfigFile returns the ConfigFile
func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
// TODO(thaJeztah): when would this happen? Is this only in tests (where cli.Initialize() is not called first?)
if cli.configFile == nil {
cli.configFile = config.LoadDefaultConfigFile(cli.err)
}
return cli.configFile
}
// ServerInfo returns the server version details for the host this client is
// connected to
func (cli *DockerCli) ServerInfo() ServerInfo {
_ = cli.initialize()
return cli.serverInfo
}
// ClientInfo returns the client details for the cli
func (cli *DockerCli) ClientInfo() ClientInfo {
return cli.clientInfo
}
// 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)
// BuildKitEnabled returns whether buildkit is enabled either through a daemon setting
// or otherwise the client-side DOCKER_BUILDKIT environment variable
func BuildKitEnabled(si ServerInfo) (bool, error) {
buildkitEnabled := si.BuildkitVersion == types.BuilderBuildKit
if buildkitEnv := os.Getenv("DOCKER_BUILDKIT"); buildkitEnv != "" {
var err error
buildkitEnabled, err = strconv.ParseBool(buildkitEnv)
if err != nil {
return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
}
return enabled, nil
}
// if a builder alias is defined, we are using BuildKit
aliasMap := cli.ConfigFile().Aliases
if _, ok := aliasMap["builder"]; ok {
return true, nil
}
si := cli.ServerInfo()
if si.BuildkitVersion == types.BuilderBuildKit {
// The daemon advertised BuildKit as the preferred builder; this may
// be either a Linux daemon or a Windows daemon with experimental
// BuildKit support enabled.
return true, nil
}
// otherwise, assume BuildKit is enabled for Linux, but disabled for
// Windows / WCOW, which does not yet support BuildKit by default.
return si.OSType != "windows", nil
}
// HooksEnabled returns whether plugin hooks are enabled.
func (cli *DockerCli) HooksEnabled() bool {
// legacy support DOCKER_CLI_HINTS env var
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
}
return enabled
}
// use DOCKER_CLI_HOOKS env var value if set and not empty
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
}
return enabled
}
featuresMap := cli.ConfigFile().Features
if v, ok := featuresMap["hooks"]; ok {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
}
return enabled
}
// default to false
return false
return buildkitEnabled, nil
}
// ManifestStore returns a store for local manifests
@ -239,14 +169,17 @@ func (cli *DockerCli) ManifestStore() manifeststore.Store {
// RegistryClient returns a client for communicating with a Docker distribution
// registry
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig {
return ResolveAuthConfig(cli.ConfigFile(), index)
resolver := func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
return ResolveAuthConfig(ctx, cli, index)
}
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
}
// InitializeOpt is the type of the functional options passed to DockerCli.Initialize
type InitializeOpt func(dockerCli *DockerCli) error
// WithInitializeClient is passed to DockerCli.Initialize by callers who wish to set a particular API Client for use by the CLI.
func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) CLIOption {
func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) InitializeOpt {
return func(dockerCli *DockerCli) error {
var err error
dockerCli.client, err = makeClient(dockerCli)
@ -256,60 +189,87 @@ func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClien
// Initialize the dockerCli runs initialization that must happen after command
// line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error {
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...InitializeOpt) error {
var err error
for _, o := range ops {
if err := o(cli); err != nil {
return err
}
}
cliflags.SetLogLevel(opts.LogLevel)
cliflags.SetLogLevel(opts.Common.LogLevel)
if opts.ConfigDir != "" {
config.SetDir(opts.ConfigDir)
cliconfig.SetDir(opts.ConfigDir)
}
if opts.Debug {
if opts.Common.Debug {
debug.Enable()
}
if opts.Context != "" && len(opts.Hosts) > 0 {
return errors.New("conflicting options: either specify --host or --context, not both")
}
cli.options = opts
cli.configFile = config.LoadDefaultConfigFile(cli.err)
cli.currentContext = resolveContextName(cli.options, cli.configFile)
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
baseContextStore := store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig)
cli.contextStore = &ContextStoreWithDefault{
Store: store.New(config.ContextStoreDir(), cli.contextStoreConfig),
Store: baseContextStore,
Resolver: func() (*DefaultContext, error) {
return ResolveDefaultContext(cli.options, cli.contextStoreConfig)
return ResolveDefaultContext(opts.Common, cli.ConfigFile(), cli.contextStoreConfig, cli.Err())
},
}
// TODO(krissetto): pass ctx to the funcs instead of using this
if cli.enableGlobalMeter {
cli.createGlobalMeterProvider(cli.baseCtx)
cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore)
if err != nil {
return err
}
if cli.enableGlobalTracer {
cli.createGlobalTracerProvider(cli.baseCtx)
cli.dockerEndpoint, err = resolveDockerEndpoint(cli.contextStore, cli.currentContext)
if err != nil {
return errors.Wrap(err, "unable to resolve docker endpoint")
}
if cli.client == nil {
cli.client, err = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile)
if tlsconfig.IsErrEncryptedKey(err) {
passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
newClient := func(password string) (client.APIClient, error) {
cli.dockerEndpoint.TLSPassword = password
return newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile)
}
cli.client, err = getClientWithPassword(passRetriever, newClient)
}
if err != nil {
return err
}
}
var experimentalValue string
// Environment variable always overrides configuration
if experimentalValue = os.Getenv("DOCKER_CLI_EXPERIMENTAL"); experimentalValue == "" {
experimentalValue = cli.configFile.Experimental
}
hasExperimental, err := isEnabled(experimentalValue)
if err != nil {
return errors.Wrap(err, "Experimental field")
}
cli.clientInfo = ClientInfo{
DefaultVersion: cli.client.ClientVersion(),
HasExperimental: hasExperimental,
}
cli.initializeFromClient()
return nil
}
// NewAPIClientFromFlags creates a new APIClient from command line flags
func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
if opts.Context != "" && len(opts.Hosts) > 0 {
return nil, errors.New("conflicting options: either specify --host or --context, not both")
}
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
storeConfig := DefaultContextStoreConfig()
contextStore := &ContextStoreWithDefault{
Store: store.New(config.ContextStoreDir(), storeConfig),
store := &ContextStoreWithDefault{
Store: store.New(cliconfig.ContextStoreDir(), storeConfig),
Resolver: func() (*DefaultContext, error) {
return ResolveDefaultContext(opts, storeConfig)
return ResolveDefaultContext(opts, configFile, storeConfig, ioutil.Discard)
},
}
endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile))
contextName, err := resolveContextName(opts, configFile, store)
if err != nil {
return nil, err
}
endpoint, err := resolveDockerEndpoint(store, contextName)
if err != nil {
return nil, errors.Wrap(err, "unable to resolve docker endpoint")
}
@ -317,21 +277,20 @@ func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.
}
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
opts, err := ep.ClientOpts()
clientOpts, err := ep.ClientOpts()
if err != nil {
return nil, err
}
if len(configFile.HTTPHeaders) > 0 {
opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders))
customHeaders := configFile.HTTPHeaders
if customHeaders == nil {
customHeaders = map[string]string{}
}
opts = append(opts, withCustomHeadersFromEnv(), client.WithUserAgent(UserAgent()))
return client.NewClientWithOpts(opts...)
customHeaders["User-Agent"] = UserAgent()
clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))
return client.NewClientWithOpts(clientOpts...)
}
func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) {
if s == nil {
return docker.Endpoint{}, errors.New("no context store initialized")
}
ctxMeta, err := s.GetMetadata(contextName)
if err != nil {
return docker.Endpoint{}, err
@ -344,7 +303,7 @@ func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint,
}
// Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) {
func resolveDefaultDockerEndpoint(opts *cliflags.CommonOptions) (docker.Endpoint, error) {
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
if err != nil {
return docker.Endpoint{}, err
@ -372,18 +331,19 @@ func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint
}, nil
}
func (cli *DockerCli) getInitTimeout() time.Duration {
if cli.initTimeout != 0 {
return cli.initTimeout
func isEnabled(value string) (bool, error) {
switch value {
case "enabled":
return true, nil
case "", "disabled":
return false, nil
default:
return false, errors.Errorf("%q is not valid, should be either enabled or disabled", value)
}
return defaultInitTimeout
}
func (cli *DockerCli) initializeFromClient() {
ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout())
defer cancel()
ping, err := cli.client.Ping(ctx)
ping, err := cli.client.Ping(context.Background())
if err != nil {
// Default to true if we fail to connect to daemon
cli.serverInfo = ServerInfo{HasExperimental: true}
@ -398,118 +358,70 @@ func (cli *DockerCli) initializeFromClient() {
HasExperimental: ping.Experimental,
OSType: ping.OSType,
BuildkitVersion: ping.BuilderVersion,
SwarmStatus: ping.SwarmStatus,
}
cli.client.NegotiateAPIVersionPing(ping)
}
func getClientWithPassword(passRetriever notary.PassRetriever, newClient func(password string) (client.APIClient, error)) (client.APIClient, error) {
for attempts := 0; ; attempts++ {
passwd, giveup, err := passRetriever("private", "encrypted TLS private", false, attempts)
if giveup || err != nil {
return nil, errors.Wrap(err, "private key is encrypted, but could not get passphrase")
}
apiclient, err := newClient(passwd)
if !tlsconfig.IsErrEncryptedKey(err) {
return apiclient, err
}
}
}
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
}
// NewContainerizedEngineClient returns a containerized engine client
func (cli *DockerCli) NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error) {
return cli.newContainerizeClient(sockPath)
}
// ContextStore returns the ContextStore
func (cli *DockerCli) ContextStore() store.Store {
return cli.contextStore
}
// CurrentContext returns the current context name, based on flags,
// environment variables and the cli configuration file, in the following
// order of preference:
//
// 1. The "--context" command-line option.
// 2. The "DOCKER_CONTEXT" environment variable ([EnvOverrideContext]).
// 3. The current context as configured through the in "currentContext"
// field in the CLI configuration file ("~/.docker/config.json").
// 4. If no context is configured, use the "default" context.
//
// # Fallbacks for backward-compatibility
//
// To preserve backward-compatibility with the "pre-contexts" behavior,
// the "default" context is used if:
//
// - The "--host" option is set
// - The "DOCKER_HOST" ([client.EnvOverrideHost]) environment variable is set
// to a non-empty value.
//
// In these cases, the default context is used, which uses the host as
// specified in "DOCKER_HOST", and TLS config from flags/env vars.
//
// Setting both the "--context" and "--host" flags is ambiguous and results
// in an error when the cli is started.
//
// CurrentContext does not validate if the given context exists or if it's
// valid; errors may occur when trying to use it.
// CurrentContext returns the current context name
func (cli *DockerCli) CurrentContext() string {
return cli.currentContext
}
// CurrentContext returns the current context name, based on flags,
// environment variables and the cli configuration file. It does not
// validate if the given context exists or if it's valid; errors may
// occur when trying to use it.
//
// Refer to [DockerCli.CurrentContext] above for further details.
func resolveContextName(opts *cliflags.ClientOptions, cfg *configfile.ConfigFile) string {
if opts != nil && opts.Context != "" {
return opts.Context
// StackOrchestrator resolves which stack orchestrator is in use
func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error) {
currentContext := cli.CurrentContext()
ctxRaw, err := cli.ContextStore().GetMetadata(currentContext)
if store.IsErrContextDoesNotExist(err) {
// case where the currentContext has been removed (CLI behavior is to fallback to using DOCKER_HOST based resolution)
return GetStackOrchestrator(flagValue, "", cli.ConfigFile().StackOrchestrator, cli.Err())
}
if opts != nil && len(opts.Hosts) > 0 {
return DefaultContextName
if err != nil {
return "", err
}
if os.Getenv(client.EnvOverrideHost) != "" {
return DefaultContextName
ctxMeta, err := GetDockerContext(ctxRaw)
if err != nil {
return "", err
}
if ctxName := os.Getenv(EnvOverrideContext); ctxName != "" {
return ctxName
}
if cfg != nil && cfg.CurrentContext != "" {
// We don't validate if this context exists: errors may occur when trying to use it.
return cfg.CurrentContext
}
return DefaultContextName
ctxOrchestrator := string(ctxMeta.StackOrchestrator)
return GetStackOrchestrator(flagValue, ctxOrchestrator, cli.ConfigFile().StackOrchestrator, cli.Err())
}
// DockerEndpoint returns the current docker endpoint
func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
if err := cli.initialize(); err != nil {
// Note that we're not terminating here, as this function may be used
// in cases where we're able to continue.
_, _ = fmt.Fprintf(cli.Err(), "%v\n", cli.initErr)
}
return cli.dockerEndpoint
}
func (cli *DockerCli) getDockerEndPoint() (ep docker.Endpoint, err error) {
cn := cli.CurrentContext()
if cn == DefaultContextName {
return resolveDefaultDockerEndpoint(cli.options)
}
return resolveDockerEndpoint(cli.contextStore, cn)
}
func (cli *DockerCli) initialize() error {
cli.init.Do(func() {
cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint()
if cli.initErr != nil {
cli.initErr = errors.Wrap(cli.initErr, "unable to resolve docker endpoint")
return
}
if cli.client == nil {
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil {
return
}
}
if cli.baseCtx == nil {
cli.baseCtx = context.Background()
}
cli.initializeFromClient()
})
return cli.initErr
}
// Apply all the operation on the cli
func (cli *DockerCli) Apply(ops ...CLIOption) error {
func (cli *DockerCli) Apply(ops ...DockerCliOption) error {
for _, op := range ops {
if err := op(cli); err != nil {
return err
@ -524,32 +436,40 @@ type ServerInfo struct {
HasExperimental bool
OSType string
BuildkitVersion types.BuilderVersion
}
// SwarmStatus provides information about the current swarm status of the
// engine, obtained from the "Swarm" header in the API response.
//
// It can be a nil struct if the API version does not provide this header
// 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 *swarm.Status
// ClientInfo stores details about the supported features of the client
type ClientInfo struct {
HasExperimental bool
DefaultVersion string
}
// NewDockerCli returns a DockerCli instance with all operators applied on it.
// It applies by default the standard streams, and the content trust from
// environment.
func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
defaultOps := []CLIOption{
// It applies by default the standard streams, the content trust from
// environment and the default containerized client constructor operations.
func NewDockerCli(ops ...DockerCliOption) (*DockerCli, error) {
cli := &DockerCli{}
defaultOps := []DockerCliOption{
WithContentTrustFromEnv(),
WithDefaultContextStoreConfig(),
WithStandardStreams(),
WithContainerizedClient(containerizedengine.NewClient),
}
cli.contextStoreConfig = DefaultContextStoreConfig()
ops = append(defaultOps, ops...)
cli := &DockerCli{baseCtx: context.Background()}
if err := cli.Apply(ops...); err != nil {
return nil, err
}
if cli.out == nil || cli.in == nil || cli.err == nil {
stdin, stdout, stderr := term.StdStreams()
if cli.in == nil {
cli.in = streams.NewIn(stdin)
}
if cli.out == nil {
cli.out = streams.NewOut(stdout)
}
if cli.err == nil {
cli.err = stderr
}
}
return cli, nil
}
@ -557,11 +477,11 @@ func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error
var host string
switch len(hosts) {
case 0:
host = os.Getenv(client.EnvOverrideHost)
host = os.Getenv("DOCKER_HOST")
case 1:
host = hosts[0]
default:
return "", errors.New("Specify only one -H")
return "", errors.New("Please specify only one -H")
}
return dopts.ParseHost(tlsOptions != nil, host)
@ -572,8 +492,42 @@ func UserAgent() string {
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
}
// resolveContextName resolves the current context name with the following rules:
// - setting both --context and --host flags is ambiguous
// - if --context is set, use this value
// - if --host flag or DOCKER_HOST is set, fallbacks to use the same logic as before context-store was added
// for backward compatibility with existing scripts
// - if DOCKER_CONTEXT is set, use this value
// - if Config file has a globally set "CurrentContext", use this value
// - fallbacks to default HOST, uses TLS config from flags/env vars
func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigFile, contextstore store.Reader) (string, error) {
if opts.Context != "" && len(opts.Hosts) > 0 {
return "", errors.New("Conflicting options: either specify --host or --context, not both")
}
if opts.Context != "" {
return opts.Context, nil
}
if len(opts.Hosts) > 0 {
return DefaultContextName, nil
}
if _, present := os.LookupEnv("DOCKER_HOST"); present {
return DefaultContextName, nil
}
if ctxName, ok := os.LookupEnv("DOCKER_CONTEXT"); ok {
return ctxName, nil
}
if config != nil && config.CurrentContext != "" {
_, err := contextstore.GetMetadata(config.CurrentContext)
if store.IsErrContextDoesNotExist(err) {
return "", errors.Errorf("Current context %q is not found on the file system, please check your config file at %s", config.CurrentContext, config.Filename)
}
return config.CurrentContext, err
}
return DefaultContextName, nil
}
var defaultStoreEndpoints = []store.NamedTypeGetter{
store.EndpointTypeGetter(docker.DockerEndpoint, func() any { return &docker.EndpointMeta{} }),
store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }),
}
// RegisterDefaultStoreEndpoints registers a new named endpoint
@ -587,7 +541,7 @@ func RegisterDefaultStoreEndpoints(ep ...store.NamedTypeGetter) {
// DefaultContextStoreConfig returns a new store.Config with the default set of endpoints configured.
func DefaultContextStoreConfig() store.Config {
return store.NewConfig(
func() any { return &DockerContext{} },
func() interface{} { return &DockerContext{} },
defaultStoreEndpoints...,
)
}

View File

@ -1,59 +1,44 @@
package command
import (
"context"
"encoding/csv"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/store"
"github.com/docker/cli/cli/streams"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/moby/term"
"github.com/pkg/errors"
clitypes "github.com/docker/cli/types"
"github.com/docker/docker/pkg/term"
)
// CLIOption is a functional argument to apply options to a [DockerCli]. These
// options can be passed to [NewDockerCli] to initialize a new CLI, or
// applied with [DockerCli.Initialize] or [DockerCli.Apply].
type CLIOption func(cli *DockerCli) error
// DockerCliOption applies a modification on a DockerCli.
type DockerCliOption func(cli *DockerCli) error
// WithStandardStreams sets a cli in, out and err streams with the standard streams.
func WithStandardStreams() CLIOption {
func WithStandardStreams() DockerCliOption {
return func(cli *DockerCli) error {
// Set terminal emulation based on platform as required.
stdin, stdout, stderr := term.StdStreams()
cli.in = streams.NewIn(stdin)
cli.out = streams.NewOut(stdout)
cli.err = streams.NewOut(stderr)
return nil
}
}
// WithBaseContext sets the base context of a cli. It is used to propagate
// the context from the command line to the client.
func WithBaseContext(ctx context.Context) CLIOption {
return func(cli *DockerCli) error {
cli.baseCtx = ctx
cli.err = stderr
return nil
}
}
// WithCombinedStreams uses the same stream for the output and error streams.
func WithCombinedStreams(combined io.Writer) CLIOption {
func WithCombinedStreams(combined io.Writer) DockerCliOption {
return func(cli *DockerCli) error {
s := streams.NewOut(combined)
cli.out = s
cli.err = s
cli.out = streams.NewOut(combined)
cli.err = combined
return nil
}
}
// WithInputStream sets a cli input stream.
func WithInputStream(in io.ReadCloser) CLIOption {
func WithInputStream(in io.ReadCloser) DockerCliOption {
return func(cli *DockerCli) error {
cli.in = streams.NewIn(in)
return nil
@ -61,7 +46,7 @@ func WithInputStream(in io.ReadCloser) CLIOption {
}
// WithOutputStream sets a cli output stream.
func WithOutputStream(out io.Writer) CLIOption {
func WithOutputStream(out io.Writer) DockerCliOption {
return func(cli *DockerCli) error {
cli.out = streams.NewOut(out)
return nil
@ -69,15 +54,15 @@ func WithOutputStream(out io.Writer) CLIOption {
}
// WithErrorStream sets a cli error stream.
func WithErrorStream(err io.Writer) CLIOption {
func WithErrorStream(err io.Writer) DockerCliOption {
return func(cli *DockerCli) error {
cli.err = streams.NewOut(err)
cli.err = err
return nil
}
}
// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
func WithContentTrustFromEnv() CLIOption {
func WithContentTrustFromEnv() DockerCliOption {
return func(cli *DockerCli) error {
cli.contentTrust = false
if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" {
@ -91,129 +76,30 @@ func WithContentTrustFromEnv() CLIOption {
}
// WithContentTrust enables content trust on a cli.
func WithContentTrust(enabled bool) CLIOption {
func WithContentTrust(enabled bool) DockerCliOption {
return func(cli *DockerCli) error {
cli.contentTrust = enabled
return nil
}
}
// WithDefaultContextStoreConfig configures the cli to use the default context store configuration.
func WithDefaultContextStoreConfig() CLIOption {
// WithContainerizedClient sets the containerized client constructor on a cli.
func WithContainerizedClient(containerizedFn func(string) (clitypes.ContainerizedClient, error)) DockerCliOption {
return func(cli *DockerCli) error {
cli.contextStoreConfig = DefaultContextStoreConfig()
cli.newContainerizeClient = containerizedFn
return nil
}
}
// WithAPIClient configures the cli to use the given API client.
func WithAPIClient(c client.APIClient) CLIOption {
// WithContextEndpointType add support for an additional typed endpoint in the context store
// Plugins should use this to store additional endpoints configuration in the context store
func WithContextEndpointType(endpointName string, endpointType store.TypeGetter) DockerCliOption {
return func(cli *DockerCli) error {
cli.client = c
switch endpointName {
case docker.DockerEndpoint:
return fmt.Errorf("cannot change %q endpoint type", endpointName)
}
cli.contextStoreConfig.SetEndpoint(endpointName, endpointType)
return nil
}
}
// envOverrideHTTPHeaders is the name of the environment-variable that can be
// used to set custom HTTP headers to be sent by the client. This environment
// variable is the equivalent to the HttpHeaders field in the configuration
// file.
//
// WARNING: If both config and environment-variable are set, the environment
// variable currently overrides all headers set in the configuration file.
// This behavior may change in a future update, as we are considering the
// environment variable to be appending to existing headers (and to only
// override headers with the same name).
//
// While this env-var allows for custom headers to be set, it does not allow
// for built-in headers (such as "User-Agent", if set) to be overridden.
// Also see [client.WithHTTPHeaders] and [client.WithUserAgent].
//
// This environment variable can be used in situations where headers must be
// set for a specific invocation of the CLI, but should not be set by default,
// and therefore cannot be set in the config-file.
//
// envOverrideHTTPHeaders accepts a comma-separated (CSV) list of key=value pairs,
// where key must be a non-empty, valid MIME header format. Whitespaces surrounding
// the key are trimmed, and the key is normalised. Whitespaces in values are
// preserved, but "key=value" pairs with an empty value (e.g. "key=") are ignored.
// Tuples without a "=" produce an error.
//
// It follows CSV rules for escaping, allowing "key=value" pairs to be quoted
// if they must contain commas, which allows for multiple values for a single
// header to be set. If a key is repeated in the list, later values override
// prior values.
//
// For example, the following value:
//
// one=one-value,"two=two,value","three= a value with whitespace ",four=,five=five=one,five=five-two
//
// Produces four headers (four is omitted as it has an empty value set):
//
// - one (value is "one-value")
// - two (value is "two,value")
// - three (value is " a value with whitespace ")
// - five (value is "five-two", the later value has overridden the prior value)
const envOverrideHTTPHeaders = "DOCKER_CUSTOM_HEADERS"
// withCustomHeadersFromEnv overriding custom HTTP headers to be sent by the
// client through the [envOverrideHTTPHeaders] environment-variable. This
// environment variable is the equivalent to the HttpHeaders field in the
// configuration file.
//
// WARNING: If both config and environment-variable are set, the environment-
// variable currently overrides all headers set in the configuration file.
// This behavior may change in a future update, as we are considering the
// environment-variable to be appending to existing headers (and to only
// override headers with the same name).
//
// TODO(thaJeztah): this is a client Option, and should be moved to the client. It is non-exported for that reason.
func withCustomHeadersFromEnv() client.Opt {
return func(apiClient *client.Client) error {
value := os.Getenv(envOverrideHTTPHeaders)
if value == "" {
return nil
}
csvReader := csv.NewReader(strings.NewReader(value))
fields, err := csvReader.Read()
if err != nil {
return errdefs.InvalidParameter(errors.Errorf("failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs", envOverrideHTTPHeaders))
}
if len(fields) == 0 {
return nil
}
env := map[string]string{}
for _, kv := range fields {
k, v, hasValue := strings.Cut(kv, "=")
// Only strip whitespace in keys; preserve whitespace in values.
k = strings.TrimSpace(k)
if k == "" {
return errdefs.InvalidParameter(errors.Errorf(`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`, envOverrideHTTPHeaders, kv))
}
// We don't currently allow empty key=value pairs, and produce an error.
// This is something we could allow in future (e.g. to read value
// from an environment variable with the same name). In the meantime,
// produce an error to prevent users from depending on this.
if !hasValue {
return errdefs.InvalidParameter(errors.Errorf(`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`, envOverrideHTTPHeaders, kv))
}
env[http.CanonicalHeaderKey(k)] = v
}
if len(env) == 0 {
// We should probably not hit this case, as we don't skip values
// (only return errors), but we don't want to discard existing
// headers with an empty set.
return nil
}
// TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
return client.WithHTTPHeaders(env)(apiClient)
}
}

View File

@ -4,11 +4,10 @@ import (
"os"
"testing"
"gotest.tools/v3/assert"
"gotest.tools/assert"
)
func contentTrustEnabled(t *testing.T) bool {
t.Helper()
var cli DockerCli
assert.NilError(t, WithContentTrustFromEnv()(&cli))
return cli.contentTrust
@ -16,13 +15,23 @@ func contentTrustEnabled(t *testing.T) bool {
// 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))
envvar := "DOCKER_CONTENT_TRUST"
if orig, ok := os.LookupEnv(envvar); ok {
defer func() {
os.Setenv(envvar, orig)
}()
} else {
defer func() {
os.Unsetenv(envvar)
}()
}
os.Setenv(envvar, "true")
assert.Assert(t, contentTrustEnabled(t))
os.Setenv(envvar, "false")
assert.Assert(t, !contentTrustEnabled(t))
os.Setenv(envvar, "invalid")
assert.Assert(t, contentTrustEnabled(t))
os.Unsetenv(envvar)
assert.Check(t, !contentTrustEnabled(t))
assert.Assert(t, !contentTrustEnabled(t))
}

View File

@ -3,28 +3,26 @@ package command
import (
"bytes"
"context"
"crypto/x509"
"fmt"
"io"
"net"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/docker/cli/cli/config"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/flags"
"github.com/docker/cli/cli/streams"
clitypes "github.com/docker/cli/types"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
"gotest.tools/v3/fs"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/env"
"gotest.tools/fs"
)
func TestNewAPIClientFromFlags(t *testing.T) {
@ -32,106 +30,72 @@ func TestNewAPIClientFromFlags(t *testing.T) {
if runtime.GOOS == "windows" {
host = "npipe://./"
}
opts := &flags.ClientOptions{Hosts: []string{host}}
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host)
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
}
func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
host := ":2375"
slug := "tcp://localhost"
if runtime.GOOS == "windows" {
slug = "tcp://127.0.0.1"
}
opts := &flags.ClientOptions{Hosts: []string{host}}
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), slug+host)
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
}
func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
var received map[string]string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
received = map[string]string{
"My-Header": r.Header.Get("My-Header"),
"User-Agent": r.Header.Get("User-Agent"),
}
_, _ = w.Write([]byte("OK"))
}))
defer ts.Close()
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
opts := &flags.ClientOptions{Hosts: []string{host}}
opts := &flags.CommonOptions{Hosts: []string{host}}
configFile := &configfile.ConfigFile{
HTTPHeaders: map[string]string{
"My-Header": "Custom-Value",
},
}
apiClient, err := NewAPIClientFromFlags(opts, configFile)
apiclient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host)
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"})
assert.Check(t, is.Equal(host, apiclient.DaemonHost()))
expectedHeaders := map[string]string{
"My-Header": "Custom-Value",
"User-Agent": UserAgent(),
}
_, err = apiClient.Ping(context.Background())
assert.NilError(t, err)
assert.DeepEqual(t, received, expectedHeaders)
assert.Check(t, is.DeepEqual(expectedHeaders, apiclient.(*client.Client).CustomHTTPHeaders()))
assert.Check(t, is.Equal(api.DefaultVersion, apiclient.ClientVersion()))
}
func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
var received http.Header
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
received = r.Header.Clone()
_, _ = w.Write([]byte("OK"))
}))
defer ts.Close()
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
opts := &flags.ClientOptions{Hosts: []string{host}}
func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
host := ":2375"
opts := &flags.CommonOptions{Hosts: []string{host}}
configFile := &configfile.ConfigFile{
HTTPHeaders: map[string]string{
"My-Header": "Custom-Value from config-file",
"My-Header": "Custom-Value",
},
}
// envOverrideHTTPHeaders should override the HTTPHeaders from the config-file,
// so "My-Header" should not be present.
t.Setenv(envOverrideHTTPHeaders, `one=one-value,"two=two,value",three=,four=four-value,four=four-value-override`)
apiClient, err := NewAPIClientFromFlags(opts, configFile)
apiclient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host)
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
assert.Check(t, is.Equal("tcp://localhost"+host, apiclient.DaemonHost()))
expectedHeaders := http.Header{
"One": []string{"one-value"},
"Two": []string{"two,value"},
"Three": []string{""},
"Four": []string{"four-value-override"},
"User-Agent": []string{UserAgent()},
expectedHeaders := map[string]string{
"My-Header": "Custom-Value",
"User-Agent": UserAgent(),
}
_, err = apiClient.Ping(context.Background())
assert.NilError(t, err)
assert.DeepEqual(t, received, expectedHeaders)
assert.Check(t, is.DeepEqual(expectedHeaders, apiclient.(*client.Client).CustomHTTPHeaders()))
assert.Check(t, is.Equal(api.DefaultVersion, apiclient.ClientVersion()))
}
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
customVersion := "v3.3.3"
t.Setenv("DOCKER_API_VERSION", customVersion)
t.Setenv("DOCKER_HOST", ":2375")
defer env.Patch(t, "DOCKER_API_VERSION", customVersion)()
defer env.Patch(t, "DOCKER_HOST", ":2375")()
opts := &flags.ClientOptions{}
opts := &flags.CommonOptions{}
configFile := &configfile.ConfigFile{}
apiclient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err)
assert.Equal(t, apiclient.ClientVersion(), customVersion)
assert.Check(t, is.Equal(customVersion, apiclient.ClientVersion()))
}
func TestNewAPIClientFromFlagsWithHttpProxyEnv(t *testing.T) {
defer env.Patch(t, "HTTP_PROXY", "http://proxy.acme.com:1234")()
defer env.Patch(t, "DOCKER_HOST", "tcp://docker.acme.com:2376")()
opts := &flags.CommonOptions{}
configFile := &configfile.ConfigFile{}
apiclient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err)
transport, ok := apiclient.HTTPClient().Transport.(*http.Transport)
assert.Assert(t, ok)
assert.Assert(t, transport.Proxy != nil)
request, err := http.NewRequest(http.MethodGet, "tcp://docker.acme.com:2376", nil)
assert.NilError(t, err)
url, err := transport.Proxy(request)
assert.NilError(t, err)
assert.Check(t, is.Equal("http://proxy.acme.com:1234", url.String()))
}
type fakeClient struct {
@ -154,9 +118,9 @@ func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
}
func TestInitializeFromClient(t *testing.T) {
const defaultVersion = "v1.55"
defaultVersion := "v1.55"
testcases := []struct {
var testcases = []struct {
doc string
pingFunc func() (types.Ping, error)
expectedServer ServerInfo
@ -188,7 +152,6 @@ func TestInitializeFromClient(t *testing.T) {
}
for _, testcase := range testcases {
testcase := testcase
t.Run(testcase.doc, func(t *testing.T) {
apiclient := &fakeClient{
pingFunc: testcase.pingFunc,
@ -196,89 +159,36 @@ func TestInitializeFromClient(t *testing.T) {
}
cli := &DockerCli{client: apiclient}
err := cli.Initialize(flags.NewClientOptions())
assert.NilError(t, err)
assert.DeepEqual(t, cli.ServerInfo(), testcase.expectedServer)
assert.Equal(t, apiclient.negotiated, testcase.negotiated)
cli.initializeFromClient()
assert.Check(t, is.DeepEqual(testcase.expectedServer, cli.serverInfo))
assert.Check(t, is.Equal(testcase.negotiated, apiclient.negotiated))
})
}
}
// Makes sure we don't hang forever on the initial connection.
// https://github.com/docker/cli/issues/3652
func TestInitializeFromClientHangs(t *testing.T) {
dir := t.TempDir()
socket := filepath.Join(dir, "my.sock")
l, err := net.Listen("unix", socket)
assert.NilError(t, err)
receiveReqCh := make(chan bool)
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Simulate a server that hangs on connections.
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case <-timeoutCtx.Done():
case receiveReqCh <- true: // Blocks until someone receives on the channel.
}
_, _ = w.Write([]byte("OK"))
}))
ts.Listener = l
ts.Start()
defer ts.Close()
opts := &flags.ClientOptions{Hosts: []string{"unix://" + socket}}
configFile := &configfile.ConfigFile{}
apiClient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err)
initializedCh := make(chan bool)
go func() {
cli := &DockerCli{client: apiClient, initTimeout: time.Millisecond}
err := cli.Initialize(flags.NewClientOptions())
assert.Check(t, err)
cli.CurrentVersion()
close(initializedCh)
}()
select {
case <-timeoutCtx.Done():
t.Fatal("timeout waiting for initialization to complete")
case <-initializedCh:
}
select {
case <-timeoutCtx.Done():
t.Fatal("server never received an init request")
case <-receiveReqCh:
}
}
// The CLI no longer disables/hides experimental CLI features, however, we need
// to verify that existing configuration files do not break
func TestExperimentalCLI(t *testing.T) {
defaultVersion := "v1.55"
testcases := []struct {
doc string
configfile string
var testcases = []struct {
doc string
configfile string
expectedExperimentalCLI bool
}{
{
doc: "default",
configfile: `{}`,
doc: "default",
configfile: `{}`,
expectedExperimentalCLI: false,
},
{
doc: "experimental",
configfile: `{
"experimental": "enabled"
}`,
expectedExperimentalCLI: true,
},
}
for _, testcase := range testcases {
testcase := testcase
t.Run(testcase.doc, func(t *testing.T) {
dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile))
defer dir.Remove()
@ -289,10 +199,77 @@ func TestExperimentalCLI(t *testing.T) {
},
}
cli := &DockerCli{client: apiclient, err: streams.NewOut(os.Stderr)}
config.SetDir(dir.Path())
cli := &DockerCli{client: apiclient, err: os.Stderr}
cliconfig.SetDir(dir.Path())
err := cli.Initialize(flags.NewClientOptions())
assert.NilError(t, err)
assert.Check(t, is.Equal(testcase.expectedExperimentalCLI, cli.ClientInfo().HasExperimental))
})
}
}
func TestGetClientWithPassword(t *testing.T) {
expected := "password"
var testcases = []struct {
doc string
password string
retrieverErr error
retrieverGiveup bool
newClientErr error
expectedErr string
}{
{
doc: "successful connect",
password: expected,
},
{
doc: "password retriever exhausted",
retrieverGiveup: true,
retrieverErr: errors.New("failed"),
expectedErr: "private key is encrypted, but could not get passphrase",
},
{
doc: "password retriever error",
retrieverErr: errors.New("failed"),
expectedErr: "failed",
},
{
doc: "newClient error",
newClientErr: errors.New("failed to connect"),
expectedErr: "failed to connect",
},
}
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
passRetriever := func(_, _ string, _ bool, attempts int) (passphrase string, giveup bool, err error) {
// Always return an invalid pass first to test iteration
switch attempts {
case 0:
return "something else", false, nil
default:
return testcase.password, testcase.retrieverGiveup, testcase.retrieverErr
}
}
newClient := func(currentPassword string) (client.APIClient, error) {
if testcase.newClientErr != nil {
return nil, testcase.newClientErr
}
if currentPassword == expected {
return &client.Client{}, nil
}
return &client.Client{}, x509.IncorrectPasswordError
}
_, err := getClientWithPassword(passRetriever, newClient)
if testcase.expectedErr != "" {
assert.ErrorContains(t, err, testcase.expectedErr)
return
}
assert.NilError(t, err)
})
}
}
@ -301,6 +278,7 @@ func TestNewDockerCliAndOperators(t *testing.T) {
// Test default operations and also overriding default ones
cli, err := NewDockerCli(
WithContentTrust(true),
WithContainerizedClient(func(string) (clitypes.ContainerizedClient, error) { return nil, nil }),
)
assert.NilError(t, err)
// Check streams are initialized
@ -308,29 +286,31 @@ func TestNewDockerCliAndOperators(t *testing.T) {
assert.Check(t, cli.Out() != nil)
assert.Check(t, cli.Err() != nil)
assert.Equal(t, cli.ContentTrustEnabled(), true)
client, err := cli.NewContainerizedEngineClient("")
assert.NilError(t, err)
assert.Equal(t, client, nil)
// Apply can modify a dockerCli after construction
inbuf := bytes.NewBuffer([]byte("input"))
outbuf := bytes.NewBuffer(nil)
errbuf := bytes.NewBuffer(nil)
err = cli.Apply(
WithInputStream(io.NopCloser(inbuf)),
cli.Apply(
WithInputStream(ioutil.NopCloser(inbuf)),
WithOutputStream(outbuf),
WithErrorStream(errbuf),
)
assert.NilError(t, err)
// Check input stream
inputStream, err := io.ReadAll(cli.In())
inputStream, err := ioutil.ReadAll(cli.In())
assert.NilError(t, err)
assert.Equal(t, string(inputStream), "input")
// Check output stream
fmt.Fprintf(cli.Out(), "output")
outputStream, err := io.ReadAll(outbuf)
outputStream, err := ioutil.ReadAll(outbuf)
assert.NilError(t, err)
assert.Equal(t, string(outputStream), "output")
// Check error stream
fmt.Fprintf(cli.Err(), "error")
errStream, err := io.ReadAll(errbuf)
errStream, err := ioutil.ReadAll(errbuf)
assert.NilError(t, err)
assert.Equal(t, string(errStream), "error")
}
@ -343,56 +323,3 @@ func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
})))
assert.Check(t, cli.ContextStore() != nil)
}
func TestHooksEnabled(t *testing.T) {
t.Run("disabled by default", func(t *testing.T) {
cli, err := NewDockerCli()
assert.NilError(t, err)
assert.Check(t, !cli.HooksEnabled())
})
t.Run("enabled in configFile", func(t *testing.T) {
configFile := `{
"features": {
"hooks": "true"
}}`
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
defer dir.Remove()
cli, err := NewDockerCli()
assert.NilError(t, err)
config.SetDir(dir.Path())
assert.Check(t, cli.HooksEnabled())
})
t.Run("env var overrides configFile", func(t *testing.T) {
configFile := `{
"features": {
"hooks": "true"
}}`
t.Setenv("DOCKER_CLI_HOOKS", "false")
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
defer dir.Remove()
cli, err := NewDockerCli()
assert.NilError(t, err)
config.SetDir(dir.Path())
assert.Check(t, !cli.HooksEnabled())
})
t.Run("legacy env var overrides configFile", func(t *testing.T) {
configFile := `{
"features": {
"hooks": "true"
}}`
t.Setenv("DOCKER_CLI_HINTS", "false")
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
defer dir.Remove()
cli, err := NewDockerCli()
assert.NilError(t, err)
config.SetDir(dir.Path())
assert.Check(t, !cli.HooksEnabled())
})
}

View File

@ -2,6 +2,7 @@ package commands
import (
"os"
"runtime"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/builder"
@ -9,6 +10,7 @@ import (
"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/engine"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/command/manifest"
"github.com/docker/cli/cli/command/network"
@ -28,52 +30,82 @@ import (
// 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
// checkpoint
checkpoint.NewCheckpointCommand(dockerCli),
// config
config.NewConfigCommand(dockerCli),
// container
container.NewContainerCommand(dockerCli),
container.NewRunCommand(dockerCli),
container.NewExecCommand(dockerCli),
container.NewPsCommand(dockerCli),
// image
image.NewImageCommand(dockerCli),
image.NewBuildCommand(dockerCli),
image.NewPullCommand(dockerCli),
image.NewPushCommand(dockerCli),
image.NewImagesCommand(dockerCli),
// builder
builder.NewBuilderCommand(dockerCli),
// manifest
manifest.NewManifestCommand(dockerCli),
// network
network.NewNetworkCommand(dockerCli),
// node
node.NewNodeCommand(dockerCli),
// plugin
plugin.NewPluginCommand(dockerCli),
// registry
registry.NewLoginCommand(dockerCli),
registry.NewLogoutCommand(dockerCli),
registry.NewSearchCommand(dockerCli),
system.NewVersionCommand(dockerCli),
system.NewInfoCommand(dockerCli),
// management commands
builder.NewBuilderCommand(dockerCli),
checkpoint.NewCheckpointCommand(dockerCli),
container.NewContainerCommand(dockerCli),
context.NewContextCommand(dockerCli),
image.NewImageCommand(dockerCli),
manifest.NewManifestCommand(dockerCli),
network.NewNetworkCommand(dockerCli),
plugin.NewPluginCommand(dockerCli),
system.NewSystemCommand(dockerCli),
trust.NewTrustCommand(dockerCli),
volume.NewVolumeCommand(dockerCli),
// orchestration (swarm) commands
config.NewConfigCommand(dockerCli),
node.NewNodeCommand(dockerCli),
// secret
secret.NewSecretCommand(dockerCli),
// service
service.NewServiceCommand(dockerCli),
// system
system.NewSystemCommand(dockerCli),
system.NewVersionCommand(dockerCli),
// stack
stack.NewStackCommand(dockerCli),
// swarm
swarm.NewSwarmCommand(dockerCli),
// trust
trust.NewTrustCommand(dockerCli),
// volume
volume.NewVolumeCommand(dockerCli),
// context
context.NewContextCommand(dockerCli),
// legacy commands may be hidden
hide(stack.NewTopLevelDeployCommand(dockerCli)),
hide(system.NewEventsCommand(dockerCli)),
hide(system.NewInfoCommand(dockerCli)),
hide(system.NewInspectCommand(dockerCli)),
hide(container.NewAttachCommand(dockerCli)),
hide(container.NewCommitCommand(dockerCli)),
hide(container.NewCopyCommand(dockerCli)),
hide(container.NewCreateCommand(dockerCli)),
hide(container.NewDiffCommand(dockerCli)),
hide(container.NewExecCommand(dockerCli)),
hide(container.NewExportCommand(dockerCli)),
hide(container.NewKillCommand(dockerCli)),
hide(container.NewLogsCommand(dockerCli)),
hide(container.NewPauseCommand(dockerCli)),
hide(container.NewPortCommand(dockerCli)),
hide(container.NewPsCommand(dockerCli)),
hide(container.NewRenameCommand(dockerCli)),
hide(container.NewRestartCommand(dockerCli)),
hide(container.NewRmCommand(dockerCli)),
@ -85,14 +117,19 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
hide(container.NewUpdateCommand(dockerCli)),
hide(container.NewWaitCommand(dockerCli)),
hide(image.NewHistoryCommand(dockerCli)),
hide(image.NewImagesCommand(dockerCli)),
hide(image.NewImportCommand(dockerCli)),
hide(image.NewLoadCommand(dockerCli)),
hide(image.NewPullCommand(dockerCli)),
hide(image.NewPushCommand(dockerCli)),
hide(image.NewRemoveCommand(dockerCli)),
hide(image.NewSaveCommand(dockerCli)),
hide(image.NewTagCommand(dockerCli)),
hide(system.NewEventsCommand(dockerCli)),
hide(system.NewInspectCommand(dockerCli)),
)
if runtime.GOOS == "linux" {
// engine
cmd.AddCommand(engine.NewEngineCommand(dockerCli))
}
}
func hide(cmd *cobra.Command) *cobra.Command {

View File

@ -1,148 +0,0 @@
package completion
import (
"os"
"strings"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
)
// ValidArgsFn a function to be used by cobra command as `ValidArgsFunction` to offer command line completion
type ValidArgsFn func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
// 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
// APIClient is needed, but we want to postpone initializing the client until
// it's used.
type APIClientProvider interface {
Client() client.APIClient
}
// ImageNames offers completion for images present within the local store
func ImageNames(dockerCLI APIClientProvider) ValidArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, err := dockerCLI.Client().ImageList(cmd.Context(), image.ListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
for _, img := range list {
names = append(names, img.RepoTags...)
}
return names, 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(types.Container) bool) ValidArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, err := dockerCLI.Client().ContainerList(cmd.Context(), container.ListOptions{
All: all,
})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
showContainerIDs := os.Getenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS") == "yes"
var names []string
for _, ctr := range list {
skip := false
for _, fn := range filters {
if !fn(ctr) {
skip = true
break
}
}
if skip {
continue
}
if showContainerIDs {
names = append(names, ctr.ID)
}
names = append(names, formatter.StripNamePrefix(ctr.Names)...)
}
return names, cobra.ShellCompDirectiveNoFileComp
}
}
// VolumeNames offers completion for volumes
func VolumeNames(dockerCLI APIClientProvider) ValidArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
for _, vol := range list.Volumes {
names = append(names, vol.Name)
}
return names, cobra.ShellCompDirectiveNoFileComp
}
}
// NetworkNames offers completion for networks
func NetworkNames(dockerCLI APIClientProvider) ValidArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
for _, nw := range list {
names = append(names, nw.Name)
}
return names, cobra.ShellCompDirectiveNoFileComp
}
}
// EnvVarNames offers completion for environment-variable names. This
// completion can be used for "--env" and "--build-arg" flags, which
// allow obtaining the value of the given environment-variable if present
// in the local environment, so we only should complete the names of the
// environment variables, and not their value. This also prevents the
// completion script from printing values of environment variables
// containing sensitive values.
//
// For example;
//
// export MY_VAR=hello
// docker run --rm --env MY_VAR alpine printenv MY_VAR
// hello
func EnvVarNames(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
envs := os.Environ()
names = make([]string, 0, len(envs))
for _, env := range envs {
name, _, _ := strings.Cut(env, "=")
names = append(names, name)
}
return names, cobra.ShellCompDirectiveNoFileComp
}
// FromList offers completion for the given list of options.
func FromList(options ...string) ValidArgsFn {
return cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp)
}
// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault],
// which indicates to let the shell perform its default behavior after
// completions have been provided.
func FileNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveDefault
}
// NoComplete is used for commands where there's no relevant completion
func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveNoFileComp
}

View File

@ -10,34 +10,34 @@ import (
type fakeClient struct {
client.Client
configCreateFunc func(context.Context, swarm.ConfigSpec) (types.ConfigCreateResponse, error)
configInspectFunc func(context.Context, string) (swarm.Config, []byte, error)
configListFunc func(context.Context, types.ConfigListOptions) ([]swarm.Config, error)
configCreateFunc func(swarm.ConfigSpec) (types.ConfigCreateResponse, error)
configInspectFunc func(string) (swarm.Config, []byte, error)
configListFunc func(types.ConfigListOptions) ([]swarm.Config, error)
configRemoveFunc func(string) error
}
func (c *fakeClient) ConfigCreate(ctx context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if c.configCreateFunc != nil {
return c.configCreateFunc(ctx, spec)
return c.configCreateFunc(spec)
}
return types.ConfigCreateResponse{}, nil
}
func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
if c.configInspectFunc != nil {
return c.configInspectFunc(ctx, id)
return c.configInspectFunc(id)
}
return swarm.Config{}, nil, nil
}
func (c *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
if c.configListFunc != nil {
return c.configListFunc(ctx, options)
return c.configListFunc(options)
}
return []swarm.Config{}, nil
}
func (c *fakeClient) ConfigRemove(_ context.Context, name string) error {
func (c *fakeClient) ConfigRemove(ctx context.Context, name string) error {
if c.configRemoveFunc != nil {
return c.configRemoveFunc(name)
}

View File

@ -1,23 +1,22 @@
package config
import (
"github.com/spf13/cobra"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/docker/api/types"
"github.com/spf13/cobra"
)
// NewConfigCommand returns a cobra command for `config` subcommands
func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Short: "Manage Swarm configs",
Short: "Manage Docker configs",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{
"version": "1.30",
"swarm": "manager",
"swarm": "",
},
}
cmd.AddCommand(
@ -28,18 +27,3 @@ func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
)
return cmd
}
// completeNames offers completion for swarm configs
func completeNames(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, err := dockerCLI.Client().ConfigList(cmd.Context(), types.ConfigListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
for _, config := range list {
names = append(names, config.ID)
}
return names, cobra.ShellCompDirectiveNoFileComp
}
}

View File

@ -4,13 +4,13 @@ import (
"context"
"fmt"
"io"
"io/ioutil"
"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/docker/docker/api/types/swarm"
"github.com/moby/sys/sequential"
"github.com/docker/docker/pkg/system"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -35,9 +35,8 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
createOpts.Name = args[0]
createOpts.File = args[1]
return RunConfigCreate(cmd.Context(), dockerCli, createOpts)
return RunConfigCreate(dockerCli, createOpts)
},
ValidArgsFunction: completion.NoComplete,
}
flags := cmd.Flags()
flags.VarP(&createOpts.Labels, "label", "l", "Config labels")
@ -48,12 +47,13 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
}
// RunConfigCreate creates a config with the given options.
func RunConfigCreate(ctx context.Context, dockerCli command.Cli, options CreateOptions) error {
func RunConfigCreate(dockerCli command.Cli, options CreateOptions) error {
client := dockerCli.Client()
ctx := context.Background()
var in io.Reader = dockerCli.In()
if options.File != "-" {
file, err := sequential.Open(options.File)
file, err := system.OpenSequential(options.File)
if err != nil {
return err
}
@ -61,7 +61,7 @@ func RunConfigCreate(ctx context.Context, dockerCli command.Cli, options CreateO
defer file.Close()
}
configData, err := io.ReadAll(in)
configData, err := ioutil.ReadAll(in)
if err != nil {
return errors.Errorf("Error reading content from %q: %v", options.File, err)
}

View File

@ -1,9 +1,7 @@
package config
import (
"context"
"io"
"os"
"io/ioutil"
"path/filepath"
"reflect"
"strings"
@ -13,9 +11,9 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/golden"
)
const configDataFile = "config-create-with-name.golden"
@ -23,38 +21,33 @@ const configDataFile = "config-create-with-name.golden"
func TestConfigCreateErrors(t *testing.T) {
testCases := []struct {
args []string
configCreateFunc func(context.Context, swarm.ConfigSpec) (types.ConfigCreateResponse, error)
configCreateFunc func(swarm.ConfigSpec) (types.ConfigCreateResponse, error)
expectedError string
}{
{
args: []string{"too_few"},
expectedError: "requires exactly 2 arguments",
},
{
args: []string{"too", "many", "arguments"},
{args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 2 arguments",
},
{
args: []string{"name", filepath.Join("testdata", configDataFile)},
configCreateFunc: func(_ context.Context, configSpec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
configCreateFunc: func(configSpec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
return types.ConfigCreateResponse{}, errors.Errorf("error creating config")
},
expectedError: "error creating config",
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.expectedError, func(t *testing.T) {
cmd := newConfigCreateCommand(
test.NewFakeCli(&fakeClient{
configCreateFunc: tc.configCreateFunc,
}),
)
cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
})
cmd := newConfigCreateCommand(
test.NewFakeCli(&fakeClient{
configCreateFunc: tc.configCreateFunc,
}),
)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
@ -62,7 +55,7 @@ func TestConfigCreateWithName(t *testing.T) {
name := "foo"
var actual []byte
cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if spec.Name != name {
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
}
@ -89,7 +82,7 @@ func TestConfigCreateWithLabels(t *testing.T) {
}
name := "foo"
data, err := os.ReadFile(filepath.Join("testdata", configDataFile))
data, err := ioutil.ReadFile(filepath.Join("testdata", configDataFile))
assert.NilError(t, err)
expected := swarm.ConfigSpec{
@ -101,7 +94,7 @@ func TestConfigCreateWithLabels(t *testing.T) {
}
cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if !reflect.DeepEqual(spec, expected) {
return types.ConfigCreateResponse{}, errors.Errorf("expected %+v, got %+v", expected, spec)
}
@ -127,7 +120,7 @@ func TestConfigCreateWithTemplatingDriver(t *testing.T) {
name := "foo"
cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if spec.Name != name {
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
}

View File

@ -101,9 +101,9 @@ func (c *configContext) Labels() string {
if mapLabels == nil {
return ""
}
joinLabels := make([]string, 0, len(mapLabels))
var joinLabels []string
for k, v := range mapLabels {
joinLabels = append(joinLabels, k+"="+v)
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
}
return strings.Join(joinLabels, ",")
}

View File

@ -7,7 +7,8 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/swarm"
"gotest.tools/v3/assert"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestConfigContextFormatWrite(t *testing.T) {
@ -19,57 +20,46 @@ func TestConfigContextFormatWrite(t *testing.T) {
// Errors
{
formatter.Context{Format: "{{InvalidFunction}}"},
`template parsing error: template: :1: function "InvalidFunction" not defined`,
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
formatter.Context{Format: "{{nil}}"},
`template parsing error: template: :1:2: executing "" at <nil>: nil is not a command`,
},
// Table format
{
formatter.Context{Format: NewFormat("table", false)},
`ID NAME CREATED UPDATED
1 passwords Less than a second ago Less than a second ago
2 id_rsa Less than a second ago Less than a second ago
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
{
formatter.Context{Format: NewFormat("table {{.Name}}", true)},
// Table format
{formatter.Context{Format: NewFormat("table", false)},
`ID NAME CREATED UPDATED
1 passwords Less than a second ago Less than a second ago
2 id_rsa Less than a second ago Less than a second ago
`},
{formatter.Context{Format: NewFormat("table {{.Name}}", true)},
`NAME
passwords
id_rsa
`,
},
{
formatter.Context{Format: NewFormat("{{.ID}}-{{.Name}}", false)},
`},
{formatter.Context{Format: NewFormat("{{.ID}}-{{.Name}}", false)},
`1-passwords
2-id_rsa
`,
},
`},
}
configs := []swarm.Config{
{
ID: "1",
{ID: "1",
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}},
},
{
ID: "2",
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"}},
},
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}}},
}
for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer
tc.context.Output = &out
if err := FormatWrite(tc.context, configs); err != nil {
assert.ErrorContains(t, err, tc.expected)
} else {
assert.Equal(t, out.String(), tc.expected)
}
})
for _, testcase := range cases {
out := bytes.NewBufferString("")
testcase.context.Output = out
if err := FormatWrite(testcase.context, configs); err != nil {
assert.ErrorContains(t, err, testcase.expected)
} else {
assert.Check(t, is.Equal(out.String(), testcase.expected))
}
}
}

View File

@ -1,17 +1,13 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.21
package config
import (
"context"
"errors"
"fmt"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
flagsHelper "github.com/docker/cli/cli/flags"
"github.com/spf13/cobra"
)
@ -30,27 +26,25 @@ func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command {
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.Names = args
return RunConfigInspect(cmd.Context(), dockerCli, opts)
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completeNames(dockerCli)(cmd, args, toComplete)
return RunConfigInspect(dockerCli, opts)
},
}
cmd.Flags().StringVarP(&opts.Format, "format", "f", "", flagsHelper.InspectFormatHelp)
cmd.Flags().StringVarP(&opts.Format, "format", "f", "", "Format the output using the given Go template")
cmd.Flags().BoolVar(&opts.Pretty, "pretty", false, "Print the information in a human friendly format")
return cmd
}
// RunConfigInspect inspects the given Swarm config.
func RunConfigInspect(ctx context.Context, dockerCli command.Cli, opts InspectOptions) error {
func RunConfigInspect(dockerCli command.Cli, opts InspectOptions) error {
client := dockerCli.Client()
ctx := context.Background()
if opts.Pretty {
opts.Format = "pretty"
}
getRef := func(id string) (any, []byte, error) {
getRef := func(id string) (interface{}, []byte, error) {
return client.ConfigInspectWithRaw(ctx, id)
}
f := opts.Format
@ -58,7 +52,7 @@ func RunConfigInspect(ctx context.Context, dockerCli command.Cli, opts InspectOp
// check if the user is trying to apply a template to the pretty format, which
// is not supported
if strings.HasPrefix(f, "pretty") && f != "pretty" {
return errors.New("cannot supply extra formatting options to the pretty template")
return fmt.Errorf("Cannot supply extra formatting options to the pretty template")
}
configCtx := formatter.Context{
@ -70,4 +64,5 @@ func RunConfigInspect(ctx context.Context, dockerCli command.Cli, opts InspectOp
return cli.StatusError{StatusCode: 1, Status: err.Error()}
}
return nil
}

View File

@ -1,25 +1,25 @@
package config
import (
"context"
"fmt"
"io"
"io/ioutil"
"testing"
"time"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/builders"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
"gotest.tools/v3/golden"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
"gotest.tools/assert"
"gotest.tools/golden"
)
func TestConfigInspectErrors(t *testing.T) {
testCases := []struct {
args []string
flags map[string]string
configInspectFunc func(_ context.Context, configID string) (swarm.Config, []byte, error)
configInspectFunc func(configID string) (swarm.Config, []byte, error)
expectedError string
}{
{
@ -27,7 +27,7 @@ func TestConfigInspectErrors(t *testing.T) {
},
{
args: []string{"foo"},
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
configInspectFunc: func(configID string) (swarm.Config, []byte, error) {
return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
},
expectedError: "error while inspecting the config",
@ -37,13 +37,13 @@ func TestConfigInspectErrors(t *testing.T) {
flags: map[string]string{
"format": "{{invalid format}}",
},
expectedError: "template parsing error",
expectedError: "Template parsing error",
},
{
args: []string{"foo", "bar"},
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
configInspectFunc: func(configID string) (swarm.Config, []byte, error) {
if configID == "foo" {
return *builders.Config(builders.ConfigName("foo")), nil, nil
return *Config(ConfigName("foo")), nil, nil
}
return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
},
@ -58,10 +58,9 @@ func TestConfigInspectErrors(t *testing.T) {
)
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
assert.Check(t, cmd.Flags().Set(key, value))
cmd.Flags().Set(key, value)
}
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
@ -70,23 +69,23 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
testCases := []struct {
name string
args []string
configInspectFunc func(_ context.Context, configID string) (swarm.Config, []byte, error)
configInspectFunc func(configID string) (swarm.Config, []byte, error)
}{
{
name: "single-config",
args: []string{"foo"},
configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
configInspectFunc: func(name string) (swarm.Config, []byte, error) {
if name != "foo" {
return swarm.Config{}, nil, errors.Errorf("Invalid name, expected %s, got %s", "foo", name)
}
return *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), nil, nil
return *Config(ConfigID("ID-foo"), ConfigName("foo")), nil, nil
},
},
{
name: "multiple-configs-with-labels",
args: []string{"foo", "bar"},
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{
configInspectFunc: func(name string) (swarm.Config, []byte, error) {
return *Config(ConfigID("ID-"+name), ConfigName(name), ConfigLabels(map[string]string{
"label1": "label-foo",
})), nil, nil
},
@ -102,8 +101,8 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
}
func TestConfigInspectWithFormat(t *testing.T) {
configInspectFunc := func(_ context.Context, name string) (swarm.Config, []byte, error) {
return *builders.Config(builders.ConfigName("foo"), builders.ConfigLabels(map[string]string{
configInspectFunc := func(name string) (swarm.Config, []byte, error) {
return *Config(ConfigName("foo"), ConfigLabels(map[string]string{
"label1": "label-foo",
})), nil, nil
}
@ -111,7 +110,7 @@ func TestConfigInspectWithFormat(t *testing.T) {
name string
format string
args []string
configInspectFunc func(_ context.Context, name string) (swarm.Config, []byte, error)
configInspectFunc func(name string) (swarm.Config, []byte, error)
}{
{
name: "simple-template",
@ -132,7 +131,7 @@ func TestConfigInspectWithFormat(t *testing.T) {
})
cmd := newConfigInspectCommand(cli)
cmd.SetArgs(tc.args)
assert.Check(t, cmd.Flags().Set("format", tc.format))
cmd.Flags().Set("format", tc.format)
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-with-format.%s.golden", tc.name))
}
@ -141,20 +140,20 @@ func TestConfigInspectWithFormat(t *testing.T) {
func TestConfigInspectPretty(t *testing.T) {
testCases := []struct {
name string
configInspectFunc func(context.Context, string) (swarm.Config, []byte, error)
configInspectFunc func(string) (swarm.Config, []byte, error)
}{
{
name: "simple",
configInspectFunc: func(_ context.Context, id string) (swarm.Config, []byte, error) {
return *builders.Config(
builders.ConfigLabels(map[string]string{
configInspectFunc: func(id string) (swarm.Config, []byte, error) {
return *Config(
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")),
ConfigID("configID"),
ConfigName("configName"),
ConfigCreatedAt(time.Time{}),
ConfigUpdatedAt(time.Time{}),
ConfigData([]byte("payload here")),
), []byte{}, nil
},
},
@ -166,7 +165,7 @@ func TestConfigInspectPretty(t *testing.T) {
cmd := newConfigInspectCommand(cli)
cmd.SetArgs([]string{"configID"})
assert.Check(t, cmd.Flags().Set("pretty", "true"))
cmd.Flags().Set("pretty", "true")
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-pretty.%s.golden", tc.name))
}

View File

@ -6,13 +6,11 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter"
flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/fvbommel/sortorder"
"github.com/spf13/cobra"
"vbom.ml/util/sortorder"
)
// ListOptions contains options for the docker config ls command.
@ -31,22 +29,22 @@ func newConfigListCommand(dockerCli command.Cli) *cobra.Command {
Short: "List configs",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return RunConfigList(cmd.Context(), dockerCli, listOpts)
return RunConfigList(dockerCli, listOpts)
},
ValidArgsFunction: completion.NoComplete,
}
flags := cmd.Flags()
flags.BoolVarP(&listOpts.Quiet, "quiet", "q", false, "Only display IDs")
flags.StringVar(&listOpts.Format, "format", "", flagsHelper.FormatHelp)
flags.StringVarP(&listOpts.Format, "format", "", "", "Pretty-print configs using a Go template")
flags.VarP(&listOpts.Filter, "filter", "f", "Filter output based on conditions provided")
return cmd
}
// RunConfigList lists Swarm configs.
func RunConfigList(ctx context.Context, dockerCli command.Cli, options ListOptions) error {
func RunConfigList(dockerCli command.Cli, options ListOptions) error {
client := dockerCli.Client()
ctx := context.Background()
configs, err := client.ConfigList(ctx, types.ConfigListOptions{Filters: options.Filter.Value()})
if err != nil {

View File

@ -1,26 +1,26 @@
package config
import (
"context"
"io"
"io/ioutil"
"testing"
"time"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/builders"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/golden"
)
func TestConfigListErrors(t *testing.T) {
testCases := []struct {
args []string
configListFunc func(context.Context, types.ConfigListOptions) ([]swarm.Config, error)
configListFunc func(types.ConfigListOptions) ([]swarm.Config, error)
expectedError string
}{
{
@ -28,7 +28,7 @@ func TestConfigListErrors(t *testing.T) {
expectedError: "accepts no argument",
},
{
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{}, errors.Errorf("error listing configs")
},
expectedError: "error listing configs",
@ -41,33 +41,32 @@ func TestConfigListErrors(t *testing.T) {
}),
)
cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestConfigList(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
configListFunc: func(options types.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)),
*Config(ConfigID("ID-1-foo"),
ConfigName("1-foo"),
ConfigVersion(swarm.Version{Index: 10}),
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
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)),
*Config(ConfigID("ID-10-foo"),
ConfigName("10-foo"),
ConfigVersion(swarm.Version{Index: 11}),
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
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)),
*Config(ConfigID("ID-2-foo"),
ConfigName("2-foo"),
ConfigVersion(swarm.Version{Index: 11}),
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
}, nil
},
@ -79,27 +78,27 @@ func TestConfigList(t *testing.T) {
func TestConfigListWithQuietOption(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
configListFunc: func(options types.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{
*Config(ConfigID("ID-foo"), ConfigName("foo")),
*Config(ConfigID("ID-bar"), ConfigName("bar"), ConfigLabels(map[string]string{
"label": "label-bar",
})),
}, nil
},
})
cmd := newConfigListCommand(cli)
assert.Check(t, cmd.Flags().Set("quiet", "true"))
cmd.Flags().Set("quiet", "true")
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-quiet-option.golden")
}
func TestConfigListWithConfigFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
configListFunc: func(options types.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{
*Config(ConfigID("ID-foo"), ConfigName("foo")),
*Config(ConfigID("ID-bar"), ConfigName("bar"), ConfigLabels(map[string]string{
"label": "label-bar",
})),
}, nil
@ -115,45 +114,45 @@ func TestConfigListWithConfigFormat(t *testing.T) {
func TestConfigListWithFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
configListFunc: func(options types.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{
*Config(ConfigID("ID-foo"), ConfigName("foo")),
*Config(ConfigID("ID-bar"), ConfigName("bar"), ConfigLabels(map[string]string{
"label": "label-bar",
})),
}, nil
},
})
cmd := newConfigListCommand(cli)
assert.Check(t, cmd.Flags().Set("format", "{{ .Name }} {{ .Labels }}"))
cmd.Flags().Set("format", "{{ .Name }} {{ .Labels }}")
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-format.golden")
}
func TestConfigListWithFilter(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
configListFunc: func(options types.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)),
*Config(ConfigID("ID-foo"),
ConfigName("foo"),
ConfigVersion(swarm.Version{Index: 10}),
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
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)),
*Config(ConfigID("ID-bar"),
ConfigName("bar"),
ConfigVersion(swarm.Version{Index: 11}),
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
}, nil
},
})
cmd := newConfigListCommand(cli)
assert.Check(t, cmd.Flags().Set("filter", "name=foo"))
assert.Check(t, cmd.Flags().Set("filter", "label=lbl1=Label-bar"))
cmd.Flags().Set("filter", "name=foo")
cmd.Flags().Set("filter", "label=lbl1=Label-bar")
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-filter.golden")
}

View File

@ -26,17 +26,15 @@ func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command {
opts := RemoveOptions{
Names: args,
}
return RunConfigRemove(cmd.Context(), dockerCli, opts)
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completeNames(dockerCli)(cmd, args, toComplete)
return RunConfigRemove(dockerCli, opts)
},
}
}
// RunConfigRemove removes the given Swarm configs.
func RunConfigRemove(ctx context.Context, dockerCli command.Cli, opts RemoveOptions) error {
func RunConfigRemove(dockerCli command.Cli, opts RemoveOptions) error {
client := dockerCli.Client()
ctx := context.Background()
var errs []string

View File

@ -1,14 +1,14 @@
package config
import (
"io"
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestConfigRemoveErrors(t *testing.T) {
@ -36,8 +36,7 @@ func TestConfigRemoveErrors(t *testing.T) {
}),
)
cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
@ -74,8 +73,7 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
cmd := newConfigRemoveCommand(cli)
cmd.SetArgs(names)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetOutput(ioutil.Discard)
assert.Error(t, cmd.Execute(), "error removing config: foo")
assert.Check(t, is.DeepEqual(names, removedConfigs))
}

View File

@ -1,4 +1,4 @@
ID NAME CREATED UPDATED
ID-1-foo 1-foo 2 hours ago About an hour ago
ID-2-foo 2-foo 2 hours ago About an hour ago
ID-10-foo 10-foo 2 hours ago About an hour ago
ID NAME CREATED UPDATED
ID-1-foo 1-foo 2 hours ago About an hour ago
ID-2-foo 2-foo 2 hours ago About an hour ago
ID-10-foo 10-foo 2 hours ago About an hour ago

View File

@ -1,3 +1,3 @@
ID NAME CREATED UPDATED
ID-bar bar 2 hours ago About an hour ago
ID-foo foo 2 hours ago About an hour ago
ID NAME CREATED UPDATED
ID-bar bar 2 hours ago About an hour ago
ID-foo foo 2 hours ago About an hour ago

View File

@ -2,29 +2,31 @@ package container
import (
"context"
"fmt"
"io"
"net/http/httputil"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/moby/sys/signal"
"github.com/docker/docker/pkg/signal"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// AttachOptions group options for `attach` command
type AttachOptions struct {
NoStdin bool
Proxy bool
DetachKeys string
type attachOptions struct {
noStdin bool
proxy bool
detachKeys string
container string
}
func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClient, args string) (*types.ContainerJSON, error) {
c, err := apiClient.ContainerInspect(ctx, args)
func inspectContainerAndCheckState(ctx context.Context, cli client.APIClient, args string) (*types.ContainerJSON, error) {
c, err := cli.ContainerInspect(ctx, args)
if err != nil {
return nil, err
}
@ -42,80 +44,69 @@ func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClie
}
// NewAttachCommand creates a new cobra.Command for `docker attach`
func NewAttachCommand(dockerCLI command.Cli) *cobra.Command {
var opts AttachOptions
func NewAttachCommand(dockerCli command.Cli) *cobra.Command {
var opts attachOptions
cmd := &cobra.Command{
Use: "attach [OPTIONS] CONTAINER",
Short: "Attach local standard input, output, and error streams to a running container",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
containerID := args[0]
return RunAttach(cmd.Context(), dockerCLI, containerID, &opts)
opts.container = args[0]
return runAttach(dockerCli, &opts)
},
Annotations: map[string]string{
"aliases": "docker container attach, docker attach",
},
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr types.Container) bool {
return ctr.State != "paused"
}),
}
flags := cmd.Flags()
flags.BoolVar(&opts.NoStdin, "no-stdin", false, "Do not attach STDIN")
flags.BoolVar(&opts.Proxy, "sig-proxy", true, "Proxy all received signals to the process")
flags.StringVar(&opts.DetachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
flags.BoolVar(&opts.noStdin, "no-stdin", false, "Do not attach STDIN")
flags.BoolVar(&opts.proxy, "sig-proxy", true, "Proxy all received signals to the process")
flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
return cmd
}
// RunAttach executes an `attach` command
func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, opts *AttachOptions) error {
apiClient := dockerCLI.Client()
func runAttach(dockerCli command.Cli, opts *attachOptions) error {
ctx := context.Background()
client := dockerCli.Client()
// request channel to wait for client
waitCtx := context.WithoutCancel(ctx)
resultC, errC := apiClient.ContainerWait(waitCtx, containerID, "")
resultC, errC := client.ContainerWait(ctx, opts.container, "")
c, err := inspectContainerAndCheckState(ctx, apiClient, containerID)
c, err := inspectContainerAndCheckState(ctx, client, opts.container)
if err != nil {
return err
}
if err := dockerCLI.In().CheckTty(!opts.NoStdin, c.Config.Tty); err != nil {
if err := dockerCli.In().CheckTty(!opts.noStdin, c.Config.Tty); err != nil {
return err
}
detachKeys := dockerCLI.ConfigFile().DetachKeys
if opts.DetachKeys != "" {
detachKeys = opts.DetachKeys
if opts.detachKeys != "" {
dockerCli.ConfigFile().DetachKeys = opts.detachKeys
}
options := container.AttachOptions{
options := types.ContainerAttachOptions{
Stream: true,
Stdin: !opts.NoStdin && c.Config.OpenStdin,
Stdin: !opts.noStdin && c.Config.OpenStdin,
Stdout: true,
Stderr: true,
DetachKeys: detachKeys,
DetachKeys: dockerCli.ConfigFile().DetachKeys,
}
var in io.ReadCloser
if options.Stdin {
in = dockerCLI.In()
in = dockerCli.In()
}
if opts.Proxy && !c.Config.Tty {
sigc := notifyAllSignals()
// since we're explicitly setting up signal handling here, and the daemon will
// get notified independently of the clients ctx cancellation, we use this context
// but without cancellation to avoid ForwardAllSignals from returning
// before all signals are forwarded.
bgCtx := context.WithoutCancel(ctx)
go ForwardAllSignals(bgCtx, apiClient, containerID, sigc)
if opts.proxy && !c.Config.Tty {
sigc := ForwardAllSignals(ctx, dockerCli, opts.container)
defer signal.StopCatch(sigc)
}
resp, errAttach := apiClient.ContainerAttach(ctx, containerID, options)
if errAttach != nil {
resp, errAttach := client.ContainerAttach(ctx, opts.container, options)
if errAttach != nil && errAttach != httputil.ErrPersistEOF {
// ContainerAttach returns an ErrPersistEOF (connection closed)
// means server met an error and put it in Hijacked connection
// keep the error and read detailed error message from hijacked connection later
return errAttach
}
defer resp.Close()
@ -128,46 +119,46 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
// the container and not exit.
//
// Recheck the container's state to avoid attach block.
_, err = inspectContainerAndCheckState(ctx, apiClient, containerID)
_, err = inspectContainerAndCheckState(ctx, client, opts.container)
if err != nil {
return err
}
if c.Config.Tty && dockerCLI.Out().IsTerminal() {
resizeTTY(ctx, dockerCLI, containerID)
if c.Config.Tty && dockerCli.Out().IsTerminal() {
resizeTTY(ctx, dockerCli, opts.container)
}
streamer := hijackedIOStreamer{
streams: dockerCLI,
streams: dockerCli,
inputStream: in,
outputStream: dockerCLI.Out(),
errorStream: dockerCLI.Err(),
outputStream: dockerCli.Out(),
errorStream: dockerCli.Err(),
resp: resp,
tty: c.Config.Tty,
detachKeys: options.DetachKeys,
}
// if the context was canceled, this was likely intentional and we shouldn't return an error
if err := streamer.stream(ctx); err != nil && !errors.Is(err, context.Canceled) {
if err := streamer.stream(ctx); err != nil {
return err
}
if errAttach != nil {
return errAttach
}
return getExitStatus(errC, resultC)
}
func getExitStatus(errC <-chan error, resultC <-chan container.WaitResponse) error {
func getExitStatus(errC <-chan error, resultC <-chan container.ContainerWaitOKBody) error {
select {
case result := <-resultC:
if result.Error != nil {
return errors.New(result.Error.Message)
return fmt.Errorf(result.Error.Message)
}
if result.StatusCode != 0 {
return cli.StatusError{StatusCode: int(result.StatusCode)}
}
case err := <-errC:
if errors.Is(err, context.Canceled) {
return nil
}
return err
}

View File

@ -1,8 +1,8 @@
package container
import (
"context"
"io"
"fmt"
"io/ioutil"
"testing"
"github.com/docker/cli/cli"
@ -10,7 +10,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
"gotest.tools/assert"
)
func TestNewAttachCommandErrors(t *testing.T) {
@ -70,27 +70,27 @@ func TestNewAttachCommandErrors(t *testing.T) {
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs(tc.args)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
})
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestGetExitStatus(t *testing.T) {
expectedErr := errors.New("unexpected error")
var (
expectedErr = fmt.Errorf("unexpected error")
errC = make(chan error, 1)
resultC = make(chan container.ContainerWaitOKBody, 1)
)
testcases := []struct {
result *container.WaitResponse
result *container.ContainerWaitOKBody
err error
expectedError error
}{
{
result: &container.WaitResponse{
result: &container.ContainerWaitOKBody{
StatusCode: 0,
},
},
@ -99,35 +99,27 @@ func TestGetExitStatus(t *testing.T) {
expectedError: expectedErr,
},
{
result: &container.WaitResponse{
Error: &container.WaitExitError{Message: expectedErr.Error()},
result: &container.ContainerWaitOKBody{
Error: &container.ContainerWaitOKBodyError{Message: expectedErr.Error()},
},
expectedError: expectedErr,
},
{
result: &container.WaitResponse{
result: &container.ContainerWaitOKBody{
StatusCode: 15,
},
expectedError: cli.StatusError{StatusCode: 15},
},
{
err: context.Canceled,
expectedError: nil,
},
}
for _, testcase := range testcases {
errC := make(chan error, 1)
resultC := make(chan container.WaitResponse, 1)
if testcase.err != nil {
errC <- testcase.err
}
if testcase.result != nil {
resultC <- *testcase.result
}
err := getExitStatus(errC, resultC)
if testcase.expectedError == nil {
assert.NilError(t, err)
} else {

View File

@ -6,42 +6,33 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/client"
specs "github.com/opencontainers/image-spec/specs-go/v1"
)
type fakeClient struct {
client.Client
inspectFunc func(string) (types.ContainerJSON, error)
execInspectFunc func(execID string) (container.ExecInspect, error)
execCreateFunc func(containerID string, options container.ExecOptions) (types.IDResponse, error)
execInspectFunc func(execID string) (types.ContainerExecInspect, error)
execCreateFunc func(container string, config types.ExecConfig) (types.IDResponse, error)
createContainerFunc func(config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
platform *specs.Platform,
containerName string) (container.CreateResponse, error)
containerStartFunc func(containerID string, options container.StartOptions) error
imageCreateFunc func(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) ([]types.Container, error)
containerName string) (container.ContainerCreateCreatedBody, error)
containerStartFunc func(container string, options types.ContainerStartOptions) error
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
infoFunc func() (types.Info, error)
containerStatPathFunc func(container, path string) (types.ContainerPathStat, error)
containerCopyFromFunc func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error)
logFunc func(string, types.ContainerLogsOptions) (io.ReadCloser, error)
waitFunc func(string) (<-chan container.ContainerWaitOKBody, <-chan error)
containerListFunc func(types.ContainerListOptions) ([]types.Container, 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
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)
containerExecResizeFunc func(id string, options types.ResizeOptions) error
Version string
}
func (f *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]types.Container, error) {
func (f *fakeClient) ContainerList(_ context.Context, options types.ContainerListOptions) ([]types.Container, error) {
if f.containerListFunc != nil {
return f.containerListFunc(options)
}
@ -55,21 +46,21 @@ func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (ty
return types.ContainerJSON{}, nil
}
func (f *fakeClient) ContainerExecCreate(_ context.Context, containerID string, config container.ExecOptions) (types.IDResponse, error) {
func (f *fakeClient) ContainerExecCreate(_ context.Context, container string, config types.ExecConfig) (types.IDResponse, error) {
if f.execCreateFunc != nil {
return f.execCreateFunc(containerID, config)
return f.execCreateFunc(container, config)
}
return types.IDResponse{}, nil
}
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (container.ExecInspect, error) {
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (types.ContainerExecInspect, error) {
if f.execInspectFunc != nil {
return f.execInspectFunc(execID)
}
return container.ExecInspect{}, nil
return types.ContainerExecInspect{}, nil
}
func (f *fakeClient) ContainerExecStart(context.Context, string, container.ExecStartOptions) error {
func (f *fakeClient) ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error {
return nil
}
@ -78,53 +69,45 @@ func (f *fakeClient) ContainerCreate(
config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
platform *specs.Platform,
containerName string,
) (container.CreateResponse, error) {
) (container.ContainerCreateCreatedBody, error) {
if f.createContainerFunc != nil {
return f.createContainerFunc(config, hostConfig, networkingConfig, platform, containerName)
return f.createContainerFunc(config, hostConfig, networkingConfig, containerName)
}
return container.CreateResponse{}, nil
return container.ContainerCreateCreatedBody{}, nil
}
func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, options container.RemoveOptions) error {
if f.containerRemoveFunc != nil {
return f.containerRemoveFunc(ctx, containerID, options)
}
return nil
}
func (f *fakeClient) ImageCreate(_ context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
if f.imageCreateFunc != nil {
return f.imageCreateFunc(parentReference, options)
}
return nil, nil
}
func (f *fakeClient) Info(_ context.Context) (system.Info, error) {
func (f *fakeClient) Info(_ context.Context) (types.Info, error) {
if f.infoFunc != nil {
return f.infoFunc()
}
return system.Info{}, nil
return types.Info{}, nil
}
func (f *fakeClient) ContainerStatPath(_ context.Context, containerID, path string) (container.PathStat, error) {
func (f *fakeClient) ContainerStatPath(_ context.Context, container, path string) (types.ContainerPathStat, error) {
if f.containerStatPathFunc != nil {
return f.containerStatPathFunc(containerID, path)
return f.containerStatPathFunc(container, path)
}
return container.PathStat{}, nil
return types.ContainerPathStat{}, nil
}
func (f *fakeClient) CopyFromContainer(_ context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
func (f *fakeClient) CopyFromContainer(_ context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
if f.containerCopyFromFunc != nil {
return f.containerCopyFromFunc(containerID, srcPath)
return f.containerCopyFromFunc(container, srcPath)
}
return nil, container.PathStat{}, nil
return nil, types.ContainerPathStat{}, nil
}
func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
func (f *fakeClient) ContainerLogs(_ context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
if f.logFunc != nil {
return f.logFunc(containerID, options)
return f.logFunc(container, options)
}
return nil, nil
}
@ -133,51 +116,30 @@ func (f *fakeClient) ClientVersion() string {
return f.Version
}
func (f *fakeClient) ContainerWait(_ context.Context, containerID string, _ container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
func (f *fakeClient) ContainerWait(_ context.Context, container string, _ container.WaitCondition) (<-chan container.ContainerWaitOKBody, <-chan error) {
if f.waitFunc != nil {
return f.waitFunc(containerID)
return f.waitFunc(container)
}
return nil, nil
}
func (f *fakeClient) ContainerStart(_ context.Context, containerID string, options container.StartOptions) error {
func (f *fakeClient) ContainerStart(_ context.Context, container string, options types.ContainerStartOptions) error {
if f.containerStartFunc != nil {
return f.containerStartFunc(containerID, options)
return f.containerStartFunc(container, options)
}
return nil
}
func (f *fakeClient) ContainerExport(_ context.Context, containerID string) (io.ReadCloser, error) {
func (f *fakeClient) ContainerExport(_ context.Context, container string) (io.ReadCloser, error) {
if f.containerExportFunc != nil {
return f.containerExportFunc(containerID)
return f.containerExportFunc(container)
}
return nil, nil
}
func (f *fakeClient) ContainerExecResize(_ context.Context, id string, options container.ResizeOptions) error {
func (f *fakeClient) ContainerExecResize(_ context.Context, id string, options types.ResizeOptions) error {
if f.containerExecResizeFunc != nil {
return f.containerExecResizeFunc(id, options)
}
return nil
}
func (f *fakeClient) ContainerKill(ctx context.Context, containerID, signal string) error {
if f.containerKillFunc != nil {
return f.containerKillFunc(ctx, containerID, signal)
}
return nil
}
func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
if f.containerPruneFunc != nil {
return f.containerPruneFunc(ctx, pruneFilters)
}
return container.PruneReport{}, nil
}
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 types.HijackedResponse{}, nil
}

View File

@ -6,9 +6,8 @@ import (
"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/docker/docker/api/types/container"
"github.com/docker/docker/api/types"
"github.com/spf13/cobra"
)
@ -35,12 +34,8 @@ func NewCommitCommand(dockerCli command.Cli) *cobra.Command {
if len(args) > 1 {
options.reference = args[1]
}
return runCommit(cmd.Context(), dockerCli, &options)
return runCommit(dockerCli, &options)
},
Annotations: map[string]string{
"aliases": "docker container commit, docker commit",
},
ValidArgsFunction: completion.ContainerNames(dockerCli, false),
}
flags := cmd.Flags()
@ -48,7 +43,7 @@ func NewCommitCommand(dockerCli command.Cli) *cobra.Command {
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>")`)
flags.StringVarP(&options.author, "author", "a", "", "Author (e.g., \"John Hannibal Smith <hannibal@a-team.com>\")")
options.changes = opts.NewListOpts(nil)
flags.VarP(&options.changes, "change", "c", "Apply Dockerfile instruction to the created image")
@ -56,14 +51,21 @@ func NewCommitCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
func runCommit(ctx context.Context, dockerCli command.Cli, options *commitOptions) error {
response, err := dockerCli.Client().ContainerCommit(ctx, options.container, container.CommitOptions{
Reference: options.reference,
func runCommit(dockerCli command.Cli, options *commitOptions) error {
ctx := context.Background()
name := options.container
reference := options.reference
commitOptions := types.ContainerCommitOptions{
Reference: reference,
Comment: options.comment,
Author: options.author,
Changes: options.changes.GetAll(),
Pause: options.pause,
})
}
response, err := dockerCli.Client().ContainerCommit(ctx, name, commitOptions)
if err != nil {
return err
}

View File

@ -1,94 +0,0 @@
package container
import (
"github.com/docker/cli/cli/command/completion"
"github.com/docker/docker/api/types/container"
"github.com/moby/sys/signal"
"github.com/spf13/cobra"
)
// allLinuxCapabilities is a list of all known Linux capabilities.
//
// This list was based on the containerd pkg/cap package;
// https://github.com/containerd/containerd/blob/v1.7.19/pkg/cap/cap_linux.go#L133-L181
//
// TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true")
var allLinuxCapabilities = []string{
"ALL", // magic value for "all capabilities"
// caps35 is the caps of kernel 3.5 (37 entries)
"CAP_CHOWN", // 2.2
"CAP_DAC_OVERRIDE", // 2.2
"CAP_DAC_READ_SEARCH", // 2.2
"CAP_FOWNER", // 2.2
"CAP_FSETID", // 2.2
"CAP_KILL", // 2.2
"CAP_SETGID", // 2.2
"CAP_SETUID", // 2.2
"CAP_SETPCAP", // 2.2
"CAP_LINUX_IMMUTABLE", // 2.2
"CAP_NET_BIND_SERVICE", // 2.2
"CAP_NET_BROADCAST", // 2.2
"CAP_NET_ADMIN", // 2.2
"CAP_NET_RAW", // 2.2
"CAP_IPC_LOCK", // 2.2
"CAP_IPC_OWNER", // 2.2
"CAP_SYS_MODULE", // 2.2
"CAP_SYS_RAWIO", // 2.2
"CAP_SYS_CHROOT", // 2.2
"CAP_SYS_PTRACE", // 2.2
"CAP_SYS_PACCT", // 2.2
"CAP_SYS_ADMIN", // 2.2
"CAP_SYS_BOOT", // 2.2
"CAP_SYS_NICE", // 2.2
"CAP_SYS_RESOURCE", // 2.2
"CAP_SYS_TIME", // 2.2
"CAP_SYS_TTY_CONFIG", // 2.2
"CAP_MKNOD", // 2.4
"CAP_LEASE", // 2.4
"CAP_AUDIT_WRITE", // 2.6.11
"CAP_AUDIT_CONTROL", // 2.6.11
"CAP_SETFCAP", // 2.6.24
"CAP_MAC_OVERRIDE", // 2.6.25
"CAP_MAC_ADMIN", // 2.6.25
"CAP_SYSLOG", // 2.6.37
"CAP_WAKE_ALARM", // 3.0
"CAP_BLOCK_SUSPEND", // 3.5
// caps316 is the caps of kernel 3.16 (38 entries)
"CAP_AUDIT_READ",
// caps58 is the caps of kernel 5.8 (40 entries)
"CAP_PERFMON",
"CAP_BPF",
// caps59 is the caps of kernel 5.9 (41 entries)
"CAP_CHECKPOINT_RESTORE",
}
// restartPolicies is a list of all valid restart-policies..
//
// TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true")
var restartPolicies = []string{
string(container.RestartPolicyDisabled),
string(container.RestartPolicyAlways),
string(container.RestartPolicyOnFailure),
string(container.RestartPolicyUnlessStopped),
}
func completeLinuxCapabilityNames(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
return completion.FromList(allLinuxCapabilities...)(cmd, args, toComplete)
}
func completeRestartPolicies(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
return completion.FromList(restartPolicies...)(cmd, args, toComplete)
}
func completeSignals(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
// TODO(thaJeztah): do we want to provide the full list here, or a subset?
signalNames := make([]string, 0, len(signal.SignalMap))
for k := range signal.SignalMap {
signalNames = append(signalNames, k)
}
return completion.FromList(signalNames...)(cmd, args, toComplete)
}

View File

@ -1,25 +1,17 @@
package container
import (
"bytes"
"context"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"strings"
"sync/atomic"
"time"
"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/docker/api/types"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/system"
units "github.com/docker/go-units"
"github.com/morikuni/aec"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -29,7 +21,6 @@ type copyOptions struct {
destination string
followLink bool
copyUIDGID bool
quiet bool
}
type copyDirection int
@ -43,85 +34,11 @@ const (
type cpConfig struct {
followLink bool
copyUIDGID bool
quiet bool
sourcePath string
destPath string
container string
}
// copyProgressPrinter wraps io.ReadCloser to print progress information when
// copying files to/from a container.
type copyProgressPrinter struct {
io.ReadCloser
total *int64
}
const (
copyToContainerHeader = "Copying to container - "
copyFromContainerHeader = "Copying from container - "
copyProgressUpdateThreshold = 75 * time.Millisecond
)
func (pt *copyProgressPrinter) Read(p []byte) (int, error) {
n, err := pt.ReadCloser.Read(p)
atomic.AddInt64(pt.total, int64(n))
return n, err
}
func copyProgress(ctx context.Context, dst io.Writer, header string, total *int64) (func(), <-chan struct{}) {
done := make(chan struct{})
if !streams.NewOut(dst).IsTerminal() {
close(done)
return func() {}, done
}
fmt.Fprint(dst, aec.Save)
fmt.Fprint(dst, "Preparing to copy...")
restore := func() {
fmt.Fprint(dst, aec.Restore)
fmt.Fprint(dst, aec.EraseLine(aec.EraseModes.All))
}
go func() {
defer close(done)
fmt.Fprint(dst, aec.Hide)
defer fmt.Fprint(dst, aec.Show)
fmt.Fprint(dst, aec.Restore)
fmt.Fprint(dst, aec.EraseLine(aec.EraseModes.All))
fmt.Fprint(dst, header)
var last int64
fmt.Fprint(dst, progressHumanSize(last))
buf := bytes.NewBuffer(nil)
ticker := time.NewTicker(copyProgressUpdateThreshold)
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
n := atomic.LoadInt64(total)
if n == last {
// Don't write to the terminal, if we don't need to.
continue
}
// Write to the buffer first to avoid flickering and context switching
fmt.Fprint(buf, aec.Column(uint(len(header)+1)))
fmt.Fprint(buf, aec.EraseLine(aec.EraseModes.Tail))
fmt.Fprint(buf, progressHumanSize(n))
buf.WriteTo(dst)
buf.Reset()
last += n
}
}
}()
return restore, done
}
// NewCopyCommand creates a new `docker cp` command
func NewCopyCommand(dockerCli command.Cli) *cobra.Command {
var opts copyOptions
@ -147,36 +64,23 @@ func NewCopyCommand(dockerCli command.Cli) *cobra.Command {
}
opts.source = args[0]
opts.destination = args[1]
if !cmd.Flag("quiet").Changed {
// User did not specify "quiet" flag; suppress output if no terminal is attached
opts.quiet = !dockerCli.Out().IsTerminal()
}
return runCopy(cmd.Context(), dockerCli, opts)
},
Annotations: map[string]string{
"aliases": "docker container cp, docker cp",
return runCopy(dockerCli, opts)
},
}
flags := cmd.Flags()
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress progress output during copy. Progress output is automatically suppressed if no terminal is attached")
return cmd
}
func progressHumanSize(n int64) string {
return units.HumanSizeWithPrecision(float64(n), 3)
}
func runCopy(ctx context.Context, dockerCli command.Cli, opts copyOptions) error {
func runCopy(dockerCli command.Cli, opts copyOptions) error {
srcContainer, srcPath := splitCpArg(opts.source)
destContainer, destPath := splitCpArg(opts.destination)
copyConfig := cpConfig{
followLink: opts.followLink,
copyUIDGID: opts.copyUIDGID,
quiet: opts.quiet,
sourcePath: srcPath,
destPath: destPath,
}
@ -191,6 +95,8 @@ func runCopy(ctx context.Context, dockerCli command.Cli, opts copyOptions) error
copyConfig.container = destContainer
}
ctx := context.Background()
switch direction {
case fromContainer:
return copyFromContainer(ctx, dockerCli, copyConfig)
@ -207,7 +113,7 @@ func resolveLocalPath(localPath string) (absPath string, err error) {
if absPath, err = filepath.Abs(localPath); err != nil {
return
}
return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil
}
func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
@ -244,10 +150,8 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cp
linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget)
srcPath = linkTarget
}
}
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
defer cancel()
}
content, stat, err := client.CopyFromContainer(ctx, copyConfig.container, srcPath)
if err != nil {
@ -267,32 +171,12 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cp
RebaseName: rebaseName,
}
var copiedSize int64
if !copyConfig.quiet {
content = &copyProgressPrinter{
ReadCloser: content,
total: &copiedSize,
}
}
preArchive := content
if len(srcInfo.RebaseName) != 0 {
_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
}
if copyConfig.quiet {
return archive.CopyTo(preArchive, srcInfo, dstPath)
}
restore, done := copyProgress(ctx, dockerCli.Err(), copyFromContainerHeader, &copiedSize)
res := archive.CopyTo(preArchive, srcInfo, dstPath)
cancel()
<-done
restore()
fmt.Fprintln(dockerCli.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", dstPath)
return res
return archive.CopyTo(preArchive, srcInfo, dstPath)
}
// In order to get the copy behavior right, we need to know information
@ -345,9 +229,8 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpCo
}
var (
content io.ReadCloser
content io.Reader
resolvedDstPath string
copiedSize int64
)
if srcPath == "-" {
@ -389,62 +272,42 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpCo
resolvedDstPath = dstDir
content = preparedArchive
if !copyConfig.quiet {
content = &copyProgressPrinter{
ReadCloser: content,
total: &copiedSize,
}
}
}
options := container.CopyToContainerOptions{
options := types.CopyToContainerOptions{
AllowOverwriteDirWithFile: false,
CopyUIDGID: copyConfig.copyUIDGID,
}
if copyConfig.quiet {
return client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
}
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
restore, done := copyProgress(ctx, dockerCli.Err(), copyToContainerHeader, &copiedSize)
res := client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
cancel()
<-done
restore()
fmt.Fprintln(dockerCli.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path)
return res
return client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
}
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
// in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by
// requiring a LOCALPATH with a `:` to be made explicit with a relative or
// absolute path:
//
// `/path/to/file:name.txt` or `./file:name.txt`
// `/path/to/file:name.txt` or `./file:name.txt`
//
// This is apparently how `scp` handles this as well:
//
// http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/
// http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/
//
// We can't simply check for a filepath separator because container names may
// have a separator, e.g., "host0/cname1" if container is in a Docker cluster,
// so we have to check for a `/` or `.` prefix. Also, in the case of a Windows
// client, a `:` could be part of an absolute Windows path, in which case it
// is immediately proceeded by a backslash.
func splitCpArg(arg string) (ctr, path string) {
func splitCpArg(arg string) (container, path string) {
if system.IsAbs(arg) {
// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
return "", arg
}
ctr, path, ok := strings.Cut(arg, ":")
if !ok || strings.HasPrefix(ctr, ".") {
parts := strings.SplitN(arg, ":", 2)
if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
// Either there's no `:` in the arg
// OR it's an explicit local relative path like `./file:name.txt`.
return "", arg
}
return ctr, path
return parts[0], parts[1]
}

View File

@ -1,24 +1,24 @@
package container
import (
"context"
"io"
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/fs"
"gotest.tools/v3/skip"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/fs"
"gotest.tools/skip"
)
func TestRunCopyWithInvalidArguments(t *testing.T) {
testcases := []struct {
var testcases = []struct {
doc string
options copyOptions
expectedErr string
@ -42,7 +42,7 @@ func TestRunCopyWithInvalidArguments(t *testing.T) {
}
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
err := runCopy(context.TODO(), test.NewFakeCli(nil), testcase.options)
err := runCopy(test.NewFakeCli(nil), testcase.options)
assert.Error(t, err, testcase.expectedErr)
})
}
@ -51,16 +51,15 @@ func TestRunCopyWithInvalidArguments(t *testing.T) {
func TestRunCopyFromContainerToStdout(t *testing.T) {
tarContent := "the tar content"
cli := test.NewFakeCli(&fakeClient{
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
assert.Check(t, is.Equal("container", ctr))
return io.NopCloser(strings.NewReader(tarContent)), container.PathStat{}, nil
fakeClient := &fakeClient{
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
assert.Check(t, is.Equal("container", container))
return ioutil.NopCloser(strings.NewReader(tarContent)), types.ContainerPathStat{}, nil
},
})
err := runCopy(context.TODO(), cli, copyOptions{
source: "container:/path",
destination: "-",
})
}
options := copyOptions{source: "container:/path", destination: "-"}
cli := test.NewFakeCli(fakeClient)
err := runCopy(cli, options)
assert.NilError(t, err)
assert.Check(t, is.Equal(tarContent, cli.OutBuffer().String()))
assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
@ -71,23 +70,21 @@ func TestRunCopyFromContainerToFilesystem(t *testing.T) {
fs.WithFile("file1", "content\n"))
defer destDir.Remove()
cli := test.NewFakeCli(&fakeClient{
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
assert.Check(t, is.Equal("container", ctr))
fakeClient := &fakeClient{
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
assert.Check(t, is.Equal("container", container))
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
return readCloser, container.PathStat{}, err
return readCloser, types.ContainerPathStat{}, err
},
})
err := runCopy(context.TODO(), cli, copyOptions{
source: "container:/path",
destination: destDir.Path(),
quiet: true,
})
}
options := copyOptions{source: "container:/path", destination: destDir.Path()}
cli := test.NewFakeCli(fakeClient)
err := runCopy(cli, options)
assert.NilError(t, err)
assert.Check(t, is.Equal("", cli.OutBuffer().String()))
assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
content, err := os.ReadFile(destDir.Join("file1"))
content, err := ioutil.ReadFile(destDir.Join("file1"))
assert.NilError(t, err)
assert.Check(t, is.Equal("content\n", string(content)))
}
@ -97,17 +94,20 @@ func TestRunCopyFromContainerToFilesystemMissingDestinationDirectory(t *testing.
fs.WithFile("file1", "content\n"))
defer destDir.Remove()
cli := test.NewFakeCli(&fakeClient{
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
assert.Check(t, is.Equal("container", ctr))
fakeClient := &fakeClient{
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
assert.Check(t, is.Equal("container", container))
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
return readCloser, container.PathStat{}, err
return readCloser, types.ContainerPathStat{}, err
},
})
err := runCopy(context.TODO(), cli, copyOptions{
}
options := copyOptions{
source: "container:/path",
destination: destDir.Join("missing", "foo"),
})
}
cli := test.NewFakeCli(fakeClient)
err := runCopy(cli, options)
assert.ErrorContains(t, err, destDir.Join("missing"))
}
@ -115,11 +115,12 @@ func TestRunCopyToContainerFromFileWithTrailingSlash(t *testing.T) {
srcFile := fs.NewFile(t, t.Name())
defer srcFile.Remove()
cli := test.NewFakeCli(&fakeClient{})
err := runCopy(context.TODO(), cli, copyOptions{
options := copyOptions{
source: srcFile.Path() + string(os.PathSeparator),
destination: "container:/path",
})
}
cli := test.NewFakeCli(&fakeClient{})
err := runCopy(cli, options)
expectedError := "not a directory"
if runtime.GOOS == "windows" {
@ -129,11 +130,12 @@ func TestRunCopyToContainerFromFileWithTrailingSlash(t *testing.T) {
}
func TestRunCopyToContainerSourceDoesNotExist(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
err := runCopy(context.TODO(), cli, copyOptions{
options := copyOptions{
source: "/does/not/exist",
destination: "container:/path",
})
}
cli := test.NewFakeCli(&fakeClient{})
err := runCopy(cli, options)
expected := "no such file or directory"
if runtime.GOOS == "windows" {
expected = "cannot find the file specified"
@ -142,7 +144,7 @@ func TestRunCopyToContainerSourceDoesNotExist(t *testing.T) {
}
func TestSplitCpArg(t *testing.T) {
testcases := []struct {
var testcases = []struct {
doc string
path string
os string
@ -178,24 +180,21 @@ func TestSplitCpArg(t *testing.T) {
expectedContainer: "container",
},
}
for _, tc := range testcases {
tc := tc
t.Run(tc.doc, func(t *testing.T) {
skip.If(t, tc.os == "windows" && runtime.GOOS != "windows" || tc.os == "linux" && runtime.GOOS == "windows")
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
skip.If(t, testcase.os != "" && testcase.os != runtime.GOOS)
ctr, path := splitCpArg(tc.path)
assert.Check(t, is.Equal(tc.expectedContainer, ctr))
assert.Check(t, is.Equal(tc.expectedPath, path))
container, path := splitCpArg(testcase.path)
assert.Check(t, is.Equal(testcase.expectedContainer, container))
assert.Check(t, is.Equal(testcase.expectedPath, path))
})
}
}
func TestRunCopyFromContainerToFilesystemIrregularDestination(t *testing.T) {
options := copyOptions{source: "container:/dev/null", destination: "/dev/random"}
cli := test.NewFakeCli(nil)
err := runCopy(context.TODO(), cli, copyOptions{
source: "container:/dev/null",
destination: "/dev/random",
})
err := runCopy(cli, options)
assert.Assert(t, err != nil)
expected := `"/dev/random" must be a directory or a regular file`
assert.ErrorContains(t, err, expected)

View File

@ -7,43 +7,30 @@ import (
"os"
"regexp"
"github.com/containerd/platforms"
"github.com/distribution/reference"
"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/streams"
"github.com/docker/cli/opts"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
imagetypes "github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/errdefs"
apiclient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/jsonmessage"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/docker/docker/registry"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// Pull constants
const (
PullImageAlways = "always"
PullImageMissing = "missing" // Default (matches previous behavior)
PullImageNever = "never"
)
type createOptions struct {
name string
platform string
untrusted bool
pull string // always, missing, never
quiet bool
}
// NewCreateCommand creates a new cobra.Command for `docker create`
func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
var options createOptions
var opts createOptions
var copts *containerOptions
cmd := &cobra.Command{
@ -55,94 +42,88 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
if len(args) > 1 {
copts.Args = args[1:]
}
return runCreate(cmd.Context(), dockerCli, cmd.Flags(), &options, copts)
return runCreate(dockerCli, cmd.Flags(), &opts, copts)
},
Annotations: map[string]string{
"aliases": "docker container create, docker create",
},
ValidArgsFunction: completion.ImageNames(dockerCli),
}
flags := cmd.Flags()
flags.SetInterspersed(false)
flags.StringVar(&options.name, "name", "", "Assign a name to the container")
flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before creating ("`+PullImageAlways+`", "|`+PullImageMissing+`", "`+PullImageNever+`")`)
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output")
flags.StringVar(&opts.name, "name", "", "Assign a name to the container")
// Add an explicit help that doesn't have a `-h` to prevent the conflict
// with hostname
flags.Bool("help", false, "Print usage")
command.AddPlatformFlag(flags, &options.platform)
command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled())
command.AddPlatformFlag(flags, &opts.platform)
command.AddTrustVerificationFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled())
copts = addFlags(flags)
_ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCli))
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
_ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
_ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCli, true))
return cmd
}
func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error {
if err := validatePullOpt(options.pull); err != nil {
reportError(dockerCli.Err(), "create", err.Error(), true)
return cli.StatusError{StatusCode: 125}
}
func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error {
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetAll()))
newEnv := []string{}
for k, v := range proxyConfig {
if v == nil {
newEnv = append(newEnv, k)
} else {
newEnv = append(newEnv, k+"="+*v)
newEnv = append(newEnv, fmt.Sprintf("%s=%s", k, *v))
}
}
copts.env = *opts.NewListOptsRef(&newEnv, nil)
containerCfg, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
containerConfig, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
if err != nil {
reportError(dockerCli.Err(), "create", err.Error(), true)
return cli.StatusError{StatusCode: 125}
}
if err = validateAPIVersion(containerCfg, dockerCli.Client().ClientVersion()); err != nil {
if err = validateAPIVersion(containerConfig, dockerCli.Client().ClientVersion()); err != nil {
reportError(dockerCli.Err(), "create", err.Error(), true)
return cli.StatusError{StatusCode: 125}
}
id, err := createContainer(ctx, dockerCli, containerCfg, options)
response, err := createContainer(context.Background(), dockerCli, containerConfig, options)
if err != nil {
return err
}
_, _ = fmt.Fprintln(dockerCli.Out(), id)
fmt.Fprintln(dockerCli.Out(), response.ID)
return nil
}
// FIXME(thaJeztah): this is the only code-path that uses APIClient.ImageCreate. Rewrite this to use the regular "pull" code (or vice-versa).
func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *createOptions) error {
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), img)
func pullImage(ctx context.Context, dockerCli command.Cli, image string, platform string, out io.Writer) error {
ref, err := reference.ParseNormalizedNamed(image)
if err != nil {
return err
}
responseBody, err := dockerCli.Client().ImageCreate(ctx, img, imagetypes.CreateOptions{
// Resolve the Repository name from fqn to RepositoryInfo
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return err
}
authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index)
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
if err != nil {
return err
}
options := types.ImageCreateOptions{
RegistryAuth: encodedAuth,
Platform: options.platform,
})
Platform: platform,
}
responseBody, err := dockerCli.Client().ImageCreate(ctx, image, options)
if err != nil {
return err
}
defer responseBody.Close()
out := dockerCli.Err()
if options.quiet {
out = streams.NewOut(io.Discard)
}
return jsonmessage.DisplayJSONMessagesToStream(responseBody, out, nil)
return jsonmessage.DisplayJSONMessagesStream(
responseBody,
out,
dockerCli.Out().FD(),
dockerCli.Out().IsTerminal(),
nil)
}
type cidFile struct {
@ -161,7 +142,7 @@ func (cid *cidFile) Close() error {
return nil
}
if err := os.Remove(cid.path); err != nil {
return errors.Wrapf(err, "failed to remove the CID file '%s'", cid.path)
return errors.Errorf("failed to remove the CID file '%s': %s \n", cid.path, err)
}
return nil
@ -172,7 +153,7 @@ func (cid *cidFile) Write(id string) error {
return nil
}
if _, err := cid.file.Write([]byte(id)); err != nil {
return errors.Wrap(err, "failed to write the container ID to the file")
return errors.Errorf("Failed to write the container ID to the file: %s", err)
}
cid.written = true
return nil
@ -183,25 +164,25 @@ func newCIDFile(path string) (*cidFile, error) {
return &cidFile{}, nil
}
if _, err := os.Stat(path); err == nil {
return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", path)
return nil, errors.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path)
}
f, err := os.Create(path)
if err != nil {
return nil, errors.Wrap(err, "failed to create the container ID file")
return nil, errors.Errorf("Failed to create the container ID file: %s", err)
}
return &cidFile{path: path, file: f}, nil
}
//nolint:gocyclo
func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *containerConfig, options *createOptions) (containerID string, err error) {
config := containerCfg.Config
hostConfig := containerCfg.HostConfig
networkingConfig := containerCfg.NetworkingConfig
func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig *containerConfig, opts *createOptions) (*container.ContainerCreateCreatedBody, error) {
config := containerConfig.Config
hostConfig := containerConfig.HostConfig
networkingConfig := containerConfig.NetworkingConfig
stderr := dockerCli.Err()
warnOnOomKillDisable(*hostConfig, dockerCli.Err())
warnOnLocalhostDNS(*hostConfig, dockerCli.Err())
warnOnOomKillDisable(*hostConfig, stderr)
warnOnLocalhostDNS(*hostConfig, stderr)
var (
trustedRef reference.Canonical
@ -210,86 +191,60 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
containerIDFile, err := newCIDFile(hostConfig.ContainerIDFile)
if err != nil {
return "", err
return nil, err
}
defer containerIDFile.Close()
ref, err := reference.ParseAnyReference(config.Image)
if err != nil {
return "", err
return nil, err
}
if named, ok := ref.(reference.Named); ok {
namedRef = reference.TagNameOnly(named)
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && !options.untrusted {
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && !opts.untrusted {
var err error
trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef)
trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef, nil)
if err != nil {
return "", err
return nil, err
}
config.Image = reference.FamiliarString(trustedRef)
}
}
pullAndTagImage := func() error {
if err := pullImage(ctx, dockerCli, config.Image, options); err != nil {
return err
}
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
return image.TagTrusted(ctx, dockerCli, trustedRef, taggedRef)
}
return nil
}
//create the container
response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, opts.name)
var platform *specs.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 "", errors.Wrap(errdefs.InvalidParameter(err), "error parsing specified platform")
}
platform = &p
}
if options.pull == PullImageAlways {
if err := pullAndTagImage(); err != nil {
return "", err
}
}
hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize()
response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
//if image not found try to pull it
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 {
if !options.quiet {
// we don't want to write to stdout anything apart from container.ID
fmt.Fprintf(dockerCli.Err(), "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
}
if apiclient.IsErrNotFound(err) && namedRef != nil {
fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
if err := pullAndTagImage(); err != nil {
return "", err
// we don't want to write to stdout anything apart from container.ID
if err := pullImage(ctx, dockerCli, config.Image, opts.platform, stderr); err != nil {
return nil, err
}
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
if err := image.TagTrusted(ctx, dockerCli, trustedRef, taggedRef); err != nil {
return nil, err
}
}
// Retry
var retryErr error
response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, opts.name)
if retryErr != nil {
return "", retryErr
return nil, retryErr
}
} else {
return "", err
return nil, err
}
}
for _, w := range response.Warnings {
_, _ = fmt.Fprintf(dockerCli.Err(), "WARNING: %s\n", w)
for _, warning := range response.Warnings {
fmt.Fprintf(stderr, "WARNING: %s\n", warning)
}
err = containerIDFile.Write(response.ID)
return response.ID, err
return &response, err
}
func warnOnOomKillDisable(hostConfig container.HostConfig, stderr io.Writer) {
@ -320,19 +275,3 @@ var localhostIPRegexp = regexp.MustCompile(ipLocalhost)
func isLocalhost(ip string) bool {
return localhostIPRegexp.MatchString(ip)
}
func validatePullOpt(val string) error {
switch val {
case PullImageAlways, PullImageMissing, PullImageNever, "":
// valid option, but nothing to do yet
return nil
default:
return fmt.Errorf(
"invalid pull option: '%s': must be one of %q, %q or %q",
val,
PullImageAlways,
PullImageMissing,
PullImageNever,
)
}
}

View File

@ -2,29 +2,27 @@ package container
import (
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"runtime"
"sort"
"strings"
"testing"
"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"
"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"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/pflag"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/fs"
"gotest.tools/v3/golden"
"github.com/pkg/errors"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/fs"
"gotest.tools/golden"
)
func TestCIDFileNoOPWithNoFilename(t *testing.T) {
@ -41,7 +39,7 @@ func TestNewCIDFileWhenFileAlreadyExists(t *testing.T) {
defer tempfile.Remove()
_, err := newCIDFile(tempfile.Path())
assert.ErrorContains(t, err, "container ID file found")
assert.ErrorContains(t, err, "Container ID file found")
}
func TestCIDFileCloseWithNoWrite(t *testing.T) {
@ -69,7 +67,7 @@ func TestCIDFileCloseWithWrite(t *testing.T) {
content := "id"
assert.NilError(t, file.Write(content))
actual, err := os.ReadFile(path)
actual, err := ioutil.ReadFile(path)
assert.NilError(t, err)
assert.Check(t, is.Equal(content, string(actual)))
@ -78,121 +76,52 @@ func TestCIDFileCloseWithWrite(t *testing.T) {
assert.NilError(t, err)
}
func TestCreateContainerImagePullPolicy(t *testing.T) {
const (
imageName = "does-not-exist-locally"
containerID = "abcdef"
)
func TestCreateContainerPullsImageIfMissing(t *testing.T) {
imageName := "does-not-exist-locally"
responseCounter := 0
containerID := "abcdef"
client := &fakeClient{
createContainerFunc: func(
config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
containerName string,
) (container.ContainerCreateCreatedBody, error) {
defer func() { responseCounter++ }()
switch responseCounter {
case 0:
return container.ContainerCreateCreatedBody{}, fakeNotFound{}
case 1:
return container.ContainerCreateCreatedBody{ID: containerID}, nil
default:
return container.ContainerCreateCreatedBody{}, errors.New("unexpected")
}
},
imageCreateFunc: func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader("")), nil
},
infoFunc: func() (types.Info, error) {
return types.Info{IndexServerAddress: "http://indexserver"}, nil
},
}
cli := test.NewFakeCli(client)
config := &containerConfig{
Config: &container.Config{
Image: imageName,
},
HostConfig: &container.HostConfig{},
}
cases := []struct {
PullPolicy string
ExpectedPulls int
ExpectedID string
ExpectedErrMsg string
ResponseCounter int
}{
{
PullPolicy: PullImageMissing,
ExpectedPulls: 1,
ExpectedID: containerID,
}, {
PullPolicy: PullImageAlways,
ExpectedPulls: 1,
ExpectedID: containerID,
ResponseCounter: 1, // This lets us return a container on the first pull
}, {
PullPolicy: PullImageNever,
ExpectedPulls: 0,
ExpectedErrMsg: "error fake not found",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.PullPolicy, func(t *testing.T) {
pullCounter := 0
client := &fakeClient{
createContainerFunc: func(
config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
platform *specs.Platform,
containerName string,
) (container.CreateResponse, error) {
defer func() { tc.ResponseCounter++ }()
switch tc.ResponseCounter {
case 0:
return container.CreateResponse{}, fakeNotFound{}
default:
return container.CreateResponse{ID: containerID}, nil
}
},
imageCreateFunc: func(parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
defer func() { pullCounter++ }()
return io.NopCloser(strings.NewReader("")), nil
},
infoFunc: func() (system.Info, error) {
return system.Info{IndexServerAddress: "https://indexserver.example.com"}, nil
},
}
fakeCLI := test.NewFakeCli(client)
id, err := createContainer(context.Background(), fakeCLI, config, &createOptions{
name: "name",
platform: runtime.GOOS,
untrusted: true,
pull: tc.PullPolicy,
})
if tc.ExpectedErrMsg != "" {
assert.Check(t, is.ErrorContains(err, tc.ExpectedErrMsg))
} else {
assert.Check(t, err)
assert.Check(t, is.Equal(tc.ExpectedID, id))
}
assert.Check(t, is.Equal(tc.ExpectedPulls, pullCounter))
})
}
}
func TestCreateContainerImagePullPolicyInvalid(t *testing.T) {
cases := []struct {
PullPolicy string
ExpectedErrMsg string
}{
{
PullPolicy: "busybox:latest",
ExpectedErrMsg: `invalid pull option: 'busybox:latest': must be one of "always", "missing" or "never"`,
},
{
PullPolicy: "--network=foo",
ExpectedErrMsg: `invalid pull option: '--network=foo': must be one of "always", "missing" or "never"`,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.PullPolicy, func(t *testing.T) {
dockerCli := test.NewFakeCli(&fakeClient{})
err := runCreate(
context.TODO(),
dockerCli,
&pflag.FlagSet{},
&createOptions{pull: tc.PullPolicy},
&containerOptions{},
)
statusErr := cli.StatusError{}
assert.Check(t, errors.As(err, &statusErr))
assert.Equal(t, statusErr.StatusCode, 125)
assert.Check(t, is.Contains(dockerCli.ErrBuffer().String(), tc.ExpectedErrMsg))
})
}
body, err := createContainer(context.Background(), cli, config, &createOptions{
name: "name",
platform: runtime.GOOS,
untrusted: true,
})
assert.NilError(t, err)
expected := container.ContainerCreateCreatedBody{ID: containerID}
assert.Check(t, is.DeepEqual(expected, *body))
stderr := cli.ErrBuffer().String()
assert.Check(t, is.Contains(stderr, "Unable to find image 'does-not-exist-locally:latest' locally"))
}
func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
@ -222,21 +151,18 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
},
}
for _, tc := range testCases {
tc := tc
fakeCLI := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCli(&fakeClient{
createContainerFunc: func(config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
platform *specs.Platform,
containerName string,
) (container.CreateResponse, error) {
return container.CreateResponse{}, errors.New("shouldn't try to pull image")
) (container.ContainerCreateCreatedBody, error) {
return container.ContainerCreateCreatedBody{}, fmt.Errorf("shouldn't try to pull image")
},
}, test.EnableContentTrust)
fakeCLI.SetNotaryClient(tc.notaryFunc)
cmd := NewCreateCommand(fakeCLI)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cli.SetNotaryClient(tc.notaryFunc)
cmd := NewCreateCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.ErrorContains(t, err, tc.expectedError)
@ -283,20 +209,18 @@ func TestNewCreateCommandWithWarnings(t *testing.T) {
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
createContainerFunc: func(config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
platform *specs.Platform,
containerName string,
) (container.CreateResponse, error) {
return container.CreateResponse{}, nil
) (container.ContainerCreateCreatedBody, error) {
return container.ContainerCreateCreatedBody{}, nil
},
})
cmd := NewCreateCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.NilError(t, err)
@ -319,36 +243,32 @@ func TestCreateContainerWithProxyConfig(t *testing.T) {
"no_proxy=noProxy",
"FTP_PROXY=ftpProxy",
"ftp_proxy=ftpProxy",
"ALL_PROXY=allProxy",
"all_proxy=allProxy",
}
sort.Strings(expected)
fakeCLI := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCli(&fakeClient{
createContainerFunc: func(config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
platform *specs.Platform,
containerName string,
) (container.CreateResponse, error) {
) (container.ContainerCreateCreatedBody, error) {
sort.Strings(config.Env)
assert.DeepEqual(t, config.Env, expected)
return container.CreateResponse{}, nil
return container.ContainerCreateCreatedBody{}, nil
},
})
fakeCLI.SetConfigFile(&configfile.ConfigFile{
cli.SetConfigFile(&configfile.ConfigFile{
Proxies: map[string]configfile.ProxyConfig{
"default": {
HTTPProxy: "httpProxy",
HTTPSProxy: "httpsProxy",
NoProxy: "noProxy",
FTPProxy: "ftpProxy",
AllProxy: "allProxy",
},
},
})
cmd := NewCreateCommand(fakeCLI)
cmd.SetOut(io.Discard)
cmd := NewCreateCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs([]string{"image:tag"})
err := cmd.Execute()
assert.NilError(t, err)
@ -356,5 +276,5 @@ func TestCreateContainerWithProxyConfig(t *testing.T) {
type fakeNotFound struct{}
func (f fakeNotFound) NotFound() {}
func (f fakeNotFound) Error() string { return "error fake not found" }
func (f fakeNotFound) NotFound() bool { return true }
func (f fakeNotFound) Error() string { return "error fake not found" }

View File

@ -5,7 +5,6 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -25,19 +24,17 @@ func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.container = args[0]
return runDiff(cmd.Context(), dockerCli, &opts)
return runDiff(dockerCli, &opts)
},
Annotations: map[string]string{
"aliases": "docker container diff, docker diff",
},
ValidArgsFunction: completion.ContainerNames(dockerCli, false),
}
}
func runDiff(ctx context.Context, dockerCli command.Cli, opts *diffOptions) error {
func runDiff(dockerCli command.Cli, opts *diffOptions) error {
if opts.container == "" {
return errors.New("Container name cannot be empty")
}
ctx := context.Background()
changes, err := dockerCli.Client().ContainerDiff(ctx, opts.container)
if err != nil {
return err

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