Compare commits

..
Author SHA1 Message Date
Andrew Hsu 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
Sebastiaan van Stijn 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
Nicolas De Loof 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Andrew Hsu 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Andrew Hsu 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
Andrew Hsu 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
Kirill Kolyshkin 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Kirill Kolyshkin 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
Sebastiaan van Stijn 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
Rohan Verma 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
Adrian Plata 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
Bret Fisher 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
Andrew Hsu 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
Andrew Hsu 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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;
https://github.com/golang/go/blob/1d0e94b1e13d5e8a323a63cd1cc1ef95290c9c36/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
Ian Campbell 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
Sebastiaan van Stijn 11b15544c5 bump docker-credential-helpers v0.6.2
full diff: https://github.com/docker/docker-credential-helpers/compare/5241b46610f2491efdf9d1c85f1ddf5b02f6d962...8a9f93a99ff87f416cc79f267c68151af0026f60

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
Silvin Lubecki 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
Andrew Hsu 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Andrew Hsu 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Andrew Hsu 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
Sebastiaan van Stijn 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
Jintao Zhang 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Brian Goff 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
Sebastiaan van Stijn 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
Goksu Toprak 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Ian Campbell 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
Ian Campbell 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
Ian Campbell 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
Ian Campbell 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
Ian Campbell 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
Ian Campbell 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
Ian Campbell 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
Ian Campbell 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
Silvin Lubecki 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
Tibor Vass 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
Tibor Vass 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
Tibor Vass 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
Ian Campbell 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
Ian Campbell 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Tibor Vass 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
Sebastiaan van Stijn 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
Tibor Vass 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Tibor Vass 394c393998 Merge pull request #1878 from thaJeztah/19.03_plugin_fixes
[19.03 backport] plugin fixes
2019-05-13 14:40:08 -07:00
Tibor Vass 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Silvin Lubecki 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
Tibor Vass 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
Tibor Vass 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
Tibor Vass 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
Ian Campbell 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
Ian Campbell 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
Ian Campbell 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
Ian Campbell 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
Ian Campbell 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
Sebastiaan van Stijn 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
Sebastiaan van Stijn 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
Harald Albers 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
Harald Albers 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
Trapier Marshall 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
Harald Albers 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
Harald Albers 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
Jintao Zhang 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
6785 changed files with 936701 additions and 753587 deletions
-20
View File
@@ -1,20 +0,0 @@
comment:
layout: header, changes, diff, sunburst
coverage:
status:
patch:
default:
target: 50%
only_pulls: true
# project will give us the diff in the total code coverage between a commit
# and its parent
project:
default:
target: auto
threshold: "15%"
changes: false
ignore:
- "**/internal/test"
- "vendor/*"
- "cli/compose/schema/bindata.go"
- ".*generated.*"
+6 -14
View File
@@ -1,14 +1,6 @@
/build/ .dockerignore
/cmd/docker/winresources/versioninfo.json .git
/cmd/docker/winresources/*.syso .gitignore
/man/man*/ appveyor.yml
/man/vendor/ build
/man/go.sum circle.yml
/docs/yaml/
/docs/vendor/
/docs/go.sum
profile.out
# top-level go.mod is not meant to be checked in
/go.mod
/go.sum
-14
View File
@@ -1,14 +0,0 @@
* text=auto
Dockerfile* linguist-language=Dockerfile
vendor.mod linguist-language=Go-Module
vendor.sum linguist-language=Go-Checksums
*.go -text diff=golang
# scripts directory contains shell scripts
# without extensions, so we need to force
scripts/** text=auto eol=lf
# shell scripts should always have LF
*.sh text eol=lf
+4 -3
View File
@@ -1,6 +1,7 @@
# GitHub code owners # GitHub code owners
# See https://github.com/blog/2392-introducing-code-owners # See https://github.com/blog/2392-introducing-code-owners
cli/command/stack/** @silvin-lubecki @docker/runtime-owners cli/command/stack/** @silvin-lubecki
contrib/completion/bash/** @albers @docker/runtime-owners contrib/completion/bash/** @albers
docs/** @thaJeztah @docker/runtime-owners contrib/completion/zsh/** @sdurrheimer
docs/** @thaJeztah
+64
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.):**
-146
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
-17
View File
@@ -1,17 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: "🗃️ Docker Sandboxes: report an issue"
about: "Issues with Docker Sandboxes should be filed in the Docker Desktop feedback tracker (not docker/cli)."
url: "https://github.com/docker/desktop-feedback/issues"
- name: "🖥️ Docker Desktop: report an issue"
about: "General Docker Desktop issues (installation, upgrades, UI, networking on macOS/Windows, WSL2, etc.) should be filed in the Docker Desktop feedback tracker (not docker/cli)."
url: "https://github.com/docker/desktop-feedback/issues"
- 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/"
@@ -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
+15 -22
View File
@@ -2,36 +2,29 @@
Please 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 https://github.com/docker/cli/blob/master/CONTRIBUTING.md
Make sure commits are signed off (`git commit -s`) with your real name. ** Make sure all your commits include a signature generated with `git commit -s` **
If the PR relates to an existing issue or PR, mention it at the top: For additional information on our contributing process, read our contributing
guide https://docs.docker.com/opensource/code/
- Fixes: https://github.com/docker/cli/issues/12345678 If this is a bug fix, make sure your description includes "fixes #xxxx", or
- Replaces: https://github.com/docker/cli/pull/12345678 "closes #xxxx"
- Follow up to: https://github.com/docker/cli/pull/12345678
- Related to: https://github.com/docker/cli/issues/12345678 Please provide the following information:
--> -->
## Summary **- What I did**
## Release notes (optional) **- How I did it**
**- How to verify it**
**- Description for the changelog**
<!-- <!--
One-sentence user-facing release note. Write a short (one line) summary that describes the changes in this
Leave empty if the change is not changelog-worthy. pull request for inclusion in the changelog:
Use present tense and imperative tone.
Good:
- Fix a panic when running `docker top` on a non-running Windows container.
- Don't print warnings in `docker info` for broken symlinks in CLI plugin directories.
Bad:
- Refactor test TestFooWithBar
- Fixed a panic when running docker top
--> -->
```markdown changelog
``` **- A picture of a cute animal (not mandatory but encouraged)**
## A picture of a cute animal (not mandatory but encouraged)
-18
View File
@@ -1,18 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
labels:
- "area/ci"
- "status/2-code-review"
cooldown:
default-days: 7
groups:
codeql-actions:
patterns:
- "github/codeql-action/*"
docker-actions:
patterns:
- "docker/*"
-198
View File
@@ -1,198 +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]+'
- '[0-9]+.x'
tags:
- 'v*'
pull_request:
jobs:
prepare:
runs-on: ubuntu-26.04
outputs:
matrix: ${{ steps.platforms.outputs.matrix }}
steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
-
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-26.04
needs:
- prepare
strategy:
fail-fast: false
matrix:
target:
- binary
- dynbinary
platform: ${{ fromJson(needs.prepare.outputs.matrix) }}
use_glibc:
- ""
- glibc
steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
-
name: Build
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
with:
targets: ${{ matrix.target }}
set: |
*.platform=${{ matrix.platform }}
env:
USE_GLIBC: ${{ matrix.use_glibc }}
-
name: Create tarball
working-directory: ./build
run: |
mkdir /tmp/out
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
env:
PLATFORM: ${{ matrix.platform }}
-
name: Upload artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.ARTIFACT_NAME }}
path: /tmp/out/*
if-no-files-found: error
bin-image:
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
uses: docker/github-builder/.github/workflows/bake.yml@58cb9f5b71b1836d6f690c1e95effdeb9b98cb8a # v1.17.0
permissions:
contents: read # same as global permission
id-token: write # for signing attestation(s) and authenticating to Docker Hub with GitHub OIDC Token
with:
setup-qemu: true
target: bin-image-cross
cache: true
cache-scope: bin-image
output: image
push: true
vars: |
VERSION=${{ github.ref }}
meta-images: |
dockereng/cli-bin
meta-tags: |
type=semver,pattern={{version}}
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
registry-identities: |
- type: dockerhub
username: dockereng
connection_id: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }}
prepare-plugins:
runs-on: ubuntu-26.04
outputs:
matrix: ${{ steps.platforms.outputs.matrix }}
steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
-
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-26.04
needs:
- prepare-plugins
strategy:
fail-fast: false
matrix:
platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }}
steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
-
name: Build
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
with:
targets: plugins-cross
set: |
*.platform=${{ matrix.platform }}
-81
View File
@@ -1,81 +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]+'
- '[0-9]+.x'
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-26.04
timeout-minutes: 10
env:
DISABLE_WARN_OUTSIDE_CONTAINER: '1'
permissions:
actions: read
contents: read
security-events: write
steps:
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 2
persist-credentials: false
# 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@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.8"
cache: false
-
name: Initialize CodeQL
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with:
languages: go
-
name: Autobuild
uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
-
name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with:
category: "/language:go"
-90
View File
@@ -1,90 +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]+'
- '[0-9]+.x'
tags:
- 'v*'
pull_request:
jobs:
tests:
runs-on: ubuntu-26.04
strategy:
fail-fast: false
matrix:
target:
- local
- connhelper-ssh
base:
- alpine
- debian
engine-version:
- rc # latest rc
- 29 # latest
- 28 # latest - 1
- 25 # mirantis lts
steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
-
name: Update daemon.json
run: |
if [ ! -f /etc/docker/daemon.json ]; then
# ubuntu 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@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
-
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@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
files: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
-50
View File
@@ -1,50 +0,0 @@
name: PR Review - Trigger
on:
pull_request:
types: [ready_for_review, opened, review_requested]
pull_request_review_comment:
types: [created]
permissions: {}
# Deduplicate simultaneous pull_request events for the same fork PR.
# When reviewers are requested at the same time, GitHub fires multiple
# review_requested events. Without this group each event triggers a
# separate review via workflow_run, producing duplicate reviews.
concurrency:
group: pr-review-trigger-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
save-context:
if: github.event.pull_request.head.repo.fork
runs-on: ubuntu-latest
steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- name: Save event context
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
COMMENT_JSON: ${{ toJSON(github.event.comment) }}
run: |
mkdir -p context
printf '%s' "${{ github.event_name }}" > context/event_name.txt
printf '%s' "$PR_NUMBER" > context/pr_number.txt
printf '%s' "$PR_HEAD_SHA" > context/pr_head_sha.txt
if [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then
printf '%s' "$COMMENT_JSON" > context/comment.json
fi
- name: Upload context
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pr-review-context
path: context/
retention-days: 1
-23
View File
@@ -1,23 +0,0 @@
name: PR Review
on:
issue_comment:
types: [created]
workflow_run:
workflows: ["PR Review - Trigger"]
types: [completed]
permissions:
contents: read
jobs:
review:
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@3654ec391b070e247de2e2e3c1ff2519b6378b3a # v2.0.9
permissions:
contents: read # Read repository files and PR diffs
pull-requests: write # Post review comments
issues: write # Create security incident issues if secrets detected
checks: write # (Optional) Show review progress as a check run
id-token: write # Required for OIDC authentication to AWS Secrets Manager
actions: write # Required since v2.0.3 — review-lock cache cleanup and feedback artifacts
with:
trigger-run-id: ${{ github.event_name == 'workflow_run' && format('{0}', github.event.workflow_run.id) || '' }}
-157
View File
@@ -1,157 +0,0 @@
name: Sync Docker release branch
concurrency:
group: ${{ github.workflow }}-${{ inputs.release_branch }}
cancel-in-progress: false
permissions:
contents: read
on:
workflow_dispatch:
inputs:
release_branch:
description: Release branch to sync, for example 29.x
required: true
type: string
tag:
description: Tag to sync from, for example v29.6.0
required: true
type: string
dry_run:
description: Merge but don't push
required: true
default: false
type: boolean
jobs:
sync-release-branch:
runs-on: ubuntu-26.04
permissions:
contents: write
actions: write
outputs:
base_sha: ${{ steps.sync.outputs.base_sha }}
has_changes: ${{ steps.sync.outputs.has_changes }}
temporary_branch: ${{ steps.sync.outputs.temporary_branch }}
temporary_sha: ${{ steps.sync.outputs.temporary_sha }}
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Validate
env:
BRANCH: ${{ github.ref_name }}
RELEASE_BRANCH: ${{ inputs.release_branch }}
run: |
if [ "$BRANCH" != "master" ]; then
echo "::error::This workflow is expected to be run on master, not $BRANCH"
exit 1
fi
if [ "$RELEASE_BRANCH" = "master" ]; then
echo "::error::The release branch must not be master"
exit 1
fi
if ! [[ "$RELEASE_BRANCH" =~ ^[0-9]+\.[x0-9]+$ ]]; then
echo "::error::Invalid release branch name: '$RELEASE_BRANCH'. Expected format: 29.x"
exit 1
fi
- name: Configure git author
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Sync release branch to tag range
id: sync
env:
DRY_RUN: ${{ inputs.dry_run }}
RELEASE_BRANCH: ${{ inputs.release_branch }}
RUN_ATTEMPT: ${{ github.run_attempt }}
RUN_ID: ${{ github.run_id }}
TAG: ${{ inputs.tag }}
run: |
set -o pipefail
base_sha=$(git rev-parse "origin/$RELEASE_BRANCH")
temporary_branch="process/sync-release-branch/$RUN_ID-$RUN_ATTEMPT"
echo "base_sha=$base_sha" >> "$GITHUB_OUTPUT"
echo "temporary_branch=$temporary_branch" >> "$GITHUB_OUTPUT"
# Keep the master checkout unchanged so scripts run from the dispatched revision.
release_worktree="$RUNNER_TEMP/release-branch"
git worktree add --detach "$release_worktree" "$base_sha"
cd "$release_worktree"
tags_file=$(mktemp)
"$GITHUB_WORKSPACE/scripts/unmerged-tags" \
"origin/$RELEASE_BRANCH" \
"$TAG" \
> "$tags_file"
echo >> "$GITHUB_STEP_SUMMARY"
echo "## Tags to sync" >> "$GITHUB_STEP_SUMMARY"
echo >> "$GITHUB_STEP_SUMMARY"
sed 's/^/- /' "$tags_file" >> "$GITHUB_STEP_SUMMARY"
xargs -r "$GITHUB_WORKSPACE/scripts/sync-branch" < "$tags_file" | tee -a "$GITHUB_STEP_SUMMARY"
if [[ "$DRY_RUN" == "true" ]]; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ $(git rev-parse HEAD) == $(git rev-parse "origin/$RELEASE_BRANCH") ]]; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No changes to push"
exit 0
fi
echo "has_changes=true" >> "$GITHUB_OUTPUT"
git push origin "HEAD:refs/heads/$temporary_branch"
echo "temporary_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
push-release-branch:
needs: sync-release-branch
if: ${{ !inputs.dry_run && needs.sync-release-branch.outputs.has_changes == 'true' }}
runs-on: ubuntu-26.04
environment: docker-releases
permissions:
contents: write
actions: write
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Push release branch
env:
BASE_SHA: ${{ needs.sync-release-branch.outputs.base_sha }}
RELEASE_BRANCH: ${{ inputs.release_branch }}
TEMPORARY_BRANCH: ${{ needs.sync-release-branch.outputs.temporary_branch }}
TEMPORARY_SHA: ${{ needs.sync-release-branch.outputs.temporary_sha }}
run: |
git fetch origin "$RELEASE_BRANCH"
current_sha=$(git rev-parse "origin/$RELEASE_BRANCH")
if [[ "$current_sha" != "$BASE_SHA" ]]; then
echo "$RELEASE_BRANCH changed from $BASE_SHA to $current_sha"
exit 1
fi
git fetch origin "$TEMPORARY_BRANCH"
current_temporary_sha=$(git rev-parse FETCH_HEAD)
if [[ "$current_temporary_sha" != "$TEMPORARY_SHA" ]]; then
echo "$TEMPORARY_BRANCH changed from $TEMPORARY_SHA to $current_temporary_sha"
exit 1
fi
git push origin "FETCH_HEAD:$RELEASE_BRANCH"
- name: Delete temporary branch
env:
TEMPORARY_BRANCH: ${{ needs.sync-release-branch.outputs.temporary_branch }}
run: git push origin --delete "$TEMPORARY_BRANCH"
-105
View File
@@ -1,105 +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]+'
- '[0-9]+.x'
tags:
- 'v*'
pull_request:
jobs:
ctn:
runs-on: ubuntu-26.04
steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
-
name: Test
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
with:
targets: test-coverage
-
name: Send to Codecov
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
files: ./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-14 # macOS 14 on arm64 (Apple Silicon M1)
- macos-15-intel # macOS 15 on Intel
- macos-15 # macOS 15 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: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: ${{ env.GOPATH }}/src/github.com/docker/cli
persist-credentials: false
-
name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.8"
cache: false
-
name: Test
run: |
# run in go modules mode to prevent traversing to nested modules
ln -s vendor.mod go.mod
ln -s vendor.sum go.sum
go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '^github.com/docker/cli/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@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
files: /tmp/coverage.txt
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
token: ${{ secrets.CODECOV_TOKEN }}
-66
View File
@@ -1,66 +0,0 @@
name: validate-milestone
permissions:
contents: read
pull-requests: read
on:
pull_request:
types: [opened, synchronize, milestoned, demilestoned, edited]
jobs:
validate-milestone:
runs-on: ubuntu-26.04
timeout-minutes: 5
steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- name: Validate milestone matches VERSION
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
MILESTONE: ${{ github.event.pull_request.milestone.title }}
with:
script: |
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
});
core.info(`Modified files: ${files.map(f => f.filename).join(', ')}`);
const touchesVersion = files.some(f => f.filename === 'VERSION');
core.info(`Touches VERSION: ${touchesVersion}`);
// Use the PR's version when it bumps the file, base branch otherwise.
// It's fine to trust the author in this case, it's not meant to be
// a security gate, just a helpful check for maintainers.
const ref = touchesVersion
? context.payload.pull_request.head.sha
: context.payload.pull_request.base.ref;
core.info(`Base ref: ${ref}`);
const resp = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: 'VERSION',
ref,
});
const expected = Buffer.from(resp.data.content, resp.data.encoding).toString('utf8').trim();
const milestone = process.env.MILESTONE;
if (!milestone) {
core.setFailed(`PR must have a milestone set (expected: ${expected})`);
return;
}
if (milestone !== expected) {
core.setFailed(`Milestone '${milestone}' does not match VERSION '${expected}'`);
return;
}
core.info(`Milestone: ${milestone} ✓`);
-117
View File
@@ -1,117 +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, synchronize]
jobs:
check-labels:
runs-on: ubuntu-26.04
timeout-minutes: 120 # guardrails timeout for the whole job
steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- name: Missing `area/` label
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
run: |
echo "::error::Every PR with an 'impact/*' label should also have an 'area/*' label"
exit 1
- name: Missing `kind/` label
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'kind/')
run: |
echo "::error::Every PR with an 'impact/*' label should also have a 'kind/*' label"
exit 1
- name: OK
run: exit 0
check-changelog:
runs-on: ubuntu-26.04
timeout-minutes: 120 # guardrails timeout for the whole job
env:
HAS_IMPACT_LABEL: ${{ contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') }}
PR_BODY: |
${{ github.event.pull_request.body }}
steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
- 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 [ "$HAS_IMPACT_LABEL" = "true" ]; then
if [ -z "$desc" ]; then
echo "::error::Changelog section is empty. Please 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
else
if [ -n "$desc" ]; then
echo "::error::PR has a changelog description, but no changelog label"
echo "::error::Please add the relevant 'impact/' label to the PR or remove the changelog description"
exit 1
fi
fi
echo "This PR will be included in the release notes with the following note:"
echo "$desc"
check-pr-branch:
runs-on: ubuntu-26.04
timeout-minutes: 120 # guardrails timeout for the whole job
env:
PR_TITLE: ${{ github.event.pull_request.title }}
steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
# 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
-143
View File
@@ -1,143 +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]+'
- '[0-9]+.x'
tags:
- 'v*'
pull_request:
jobs:
validate:
runs-on: ubuntu-26.04
strategy:
fail-fast: false
matrix:
target:
- lint
- shellcheck
- validate-vendor
- update-authors # ensure authors update target runs fine
steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
-
name: Run
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
with:
targets: ${{ matrix.target }}
# check that the generated Markdown and the checked-in files match
validate-md:
runs-on: ubuntu-26.04
steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
-
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-26.04
strategy:
fail-fast: false
matrix:
target:
- yamldocs # ensure yamldocs target runs fine
- manpages # ensure manpages target runs fine
steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
-
name: Run
shell: 'script --return --quiet --command "bash {0}"'
run: |
make -f docker.Makefile ${{ matrix.target }}
validate-gocompat:
runs-on: ubuntu-26.04
env:
GOPATH: ${{ github.workspace }}
GO111MODULE: off
steps:
- name: Configure credentials
continue-on-error: true
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
with:
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
profile: administrator
profile-region: us-east-1
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: src/github.com/docker/cli
persist-credentials: false
-
name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.8"
cache: false
-
name: Run gocompat check
shell: 'script --return --quiet --command "bash {0}"'
working-directory: ${{ github.workspace }}/src/github.com/docker/cli
run: |
make -C ./internal/gocompat verify
-31
View File
@@ -1,31 +0,0 @@
name: zizmor
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
on:
workflow_dispatch:
push:
branches:
- 'main'
- 'master'
- '[0-9]+.[0-9]+'
- '[0-9]+.x'
tags:
- 'v*'
pull_request:
jobs:
run:
uses: crazy-max/.github/.github/workflows/zizmor.yml@f91220cba053bd54299ba645a599ee8ed436f222 # v1.11.0
permissions:
contents: read
security-events: write
with:
min-severity: medium
min-confidence: medium
persona: pedantic
+8 -7
View File
@@ -1,5 +1,5 @@
# if you want to ignore files created by your editor/tools, # 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
*.exe~ *.exe~
*.orig *.orig
@@ -8,10 +8,11 @@
Thumbs.db Thumbs.db
.editorconfig .editorconfig
/build/ /build/
/cmd/docker/winresources/versioninfo.json cli/winresources/rsrc_386.syso
/cmd/docker/winresources/*.syso cli/winresources/rsrc_amd64.syso
/man/man1/
/man/man5/
/man/man8/
/docs/yaml/gen/
coverage.txt
profile.out profile.out
# top-level go.mod is not meant to be checked in
/go.mod
/go.sum
-237
View File
@@ -1,237 +0,0 @@
version: "2"
run:
# prevent golangci-lint from deducting the go version to lint for through go.mod,
# which causes it to fallback to go1.17 semantics.
#
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
go: "1.26.8"
timeout: 5m
issues:
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-issues-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0
formatters:
enable:
- gofumpt # Detects whether code was gofumpt-ed.
- goimports
exclusions:
generated: strict
linters:
enable:
- asasalint # Detects "[]any" used as argument for variadic "func(...any)".
- bodyclose
- copyloopvar # Detects places where loop variables are copied.
- depguard
- dogsled # Detects assignments with too many blank identifiers.
- dupword # Detects duplicate words.
- durationcheck # Detect cases where two time.Duration values are being multiplied in possibly erroneous ways.
- errcheck
- errchkjson # Detects unsupported types passed to json encoding functions and reports if checks for the returned error can be omitted.
- exhaustive # Detects missing options in enum switch statements.
- exptostd # Detects functions from golang.org/x/exp/ that can be replaced by std functions.
- fatcontext # Detects nested contexts in loops and function literals.
- forbidigo
- gocheckcompilerdirectives # Detects invalid go compiler directive comments (//go:).
- gocritic # Metalinter; detects bugs, performance, and styling issues.
- gocyclo
- gosec # Detects security problems.
- govet
- iface # Detects incorrect use of interfaces. Currently only used for "identical" interfaces in the same package.
- importas # Enforces consistent import aliases.
- ineffassign
- makezero # Finds slice declarations with non-zero initial length.
- mirror # Detects wrong mirror patterns of bytes/strings usage.
- misspell # Detects commonly misspelled English words in comments.
- nakedret # Detects uses of naked returns.
- nilnesserr # Detects returning nil errors. It combines the features of nilness and nilerr,
- nosprintfhostport # Detects misuse of Sprintf to construct a host with port in a URL.
- 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 # Detects reassigning a top-level variable in another package.
- revive # Metalinter; drop-in replacement for golint.
- spancheck # Detects mistakes with OpenTelemetry/Census spans.
- staticcheck
- thelper # Detects test helpers without t.Helper().
- tparallel # Detects inappropriate usage of t.Parallel().
- unconvert # Detects unnecessary type conversions.
- unparam
- unused
- usestdlibvars # Detects the possibility to use variables/constants from the Go standard library.
- usetesting # Reports uses of functions with replacement inside the testing package.
- wastedassign # Detects wasted assignment statements.
disable:
- errcheck
settings:
depguard:
rules:
main:
deny:
- pkg: "github.com/containerd/containerd/errdefs"
desc: The containerd errdefs package was migrated to a separate module. Use github.com/containerd/errdefs instead.
- pkg: "github.com/containerd/containerd/log"
desc: The containerd log package was migrated to a separate module. Use github.com/containerd/log instead.
- pkg: "github.com/containerd/containerd/pkg/userns"
desc: Use github.com/moby/sys/userns instead.
- pkg: "github.com/containerd/containerd/platforms"
desc: The containerd platforms package was migrated to a separate module. Use github.com/containerd/platforms instead.
- pkg: "github.com/docker/docker/errdefs"
desc: Use github.com/containerd/errdefs instead.
- pkg: "github.com/docker/docker/pkg/system"
desc: This package should not be used unless strictly necessary.
- pkg: "github.com/docker/distribution/uuid"
desc: Use github.com/google/uuid instead.
- pkg: "io/ioutil"
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
- pkg: "gopkg.in/yaml.v3"
desc: Use go.yaml.in/yaml/v3 instead.
forbidigo:
forbid:
- pkg: ^regexp$
pattern: ^regexp\.MustCompile
msg: Use internal/lazyregexp.New instead.
gocyclo:
min-complexity: 16
gosec:
excludes:
- G104 # G104: Errors unhandled; (TODO: reduce unhandled errors, or explicitly ignore)
- G115 # G115: integer overflow conversion; (TODO: verify these: https://github.com/docker/cli/issues/5584)
- G117 # G117: Exported struct field matches secret pattern (false positives for legitimate field names)
- G118 # G118: Goroutine uses context.Background/TODO while request-scoped context is available (TODO: evaluate these)
- G122 # G122: Filesystem operation in filepath.Walk/WalkDir callback uses race-prone path (TODO: evaluate these)
- G306 # G306: Expect WriteFile permissions to be 0600 or less (too restrictive; also flags "0o644" permissions)
- G307 # G307: Deferring unsafe method "*os.File" on type "Close" (also EXC0008); (TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close")
- G702 # G702: Command injection via taint analysis (TODO: evaluate these)
- G703 # G703: Path traversal via taint analysis (TODO: evaluate these)
- G704 # G704: SSRF via taint analysis (TODO: evaluate these)
- G705 # G705: XSS via taint analysis (TODO: evaluate these)
govet:
enable:
- shadow
settings:
shadow:
strict: true
lll:
line-length: 200
importas:
# Do not allow unaliased imports of aliased packages.
no-unaliased: true
alias:
# Should no longer be aliased, because we no longer allow moby/docker errdefs.
- pkg: "github.com/docker/docker/errdefs"
alias: ""
- pkg: github.com/opencontainers/image-spec/specs-go/v1
alias: ocispec
# Enforce that gotest.tools/v3/assert/cmp is always aliased as "is"
- pkg: gotest.tools/v3/assert/cmp
alias: is
nakedret:
# Disallow naked returns if func has more lines of code than this setting.
# Default: 30
max-func-lines: 0
staticcheck:
checks:
- all
- -QF1008 # Omit embedded fields from selector expression; https://staticcheck.dev/docs/checks/#QF1008
revive:
rules:
- name: empty-block # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block
- name: empty-lines # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
- name: import-shadowing # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing
- name: line-length-limit # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#line-length-limit
arguments: [200]
- name: unused-receiver # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver
- name: use-any # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
- name: use-errors-new # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#use-errors-new
exclusions:
# We prefer to use an "linters.exclusions.rules" 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 excludes at:
# https://github.com/golangci/golangci-lint/blob/v1.61.0/pkg/config/issues.go#L11-L104
#
# The default list of exclusions can be found at:
# https://golangci-lint.run/usage/false-positives/#default-exclusions
generated: strict
rules:
# EXC0003
- text: "func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this"
linters:
- revive
# EXC0007
- text: "Subprocess launch(ed with variable|ing should be audited)"
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
# TODO: make sure all packages have a description. Currently, there's 67 packages without.
- text: "package-comments: should have a package comment"
linters:
- revive
# 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:
- staticcheck
# 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
# Ignore for cli/command/formatter/tabwriter, which is forked from go stdlib, so we want to align with it.
- text: '^(ST1020|ST1022): comment on exported'
path: "cli/command/formatter/tabwriter"
linters:
- staticcheck
# TODO(thaJeztah): remove once https://github.com/leighmcculloch/gocheckcompilerdirectives/issues/7 is fixed.
- text: "compiler directive unrecognized: //go:fix"
linters:
- gocheckcompilerdirectives
# Log a warning if an exclusion rule is unused.
# Default: false
warn-unused: true
+27 -159
View File
@@ -6,27 +6,16 @@
# #
# For explanation on this file format: man git-shortlog # 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 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> Abhinandan Prativadi <abhi@docker.com>
Ace Tang <aceapril@126.com> Ace Tang <aceapril@126.com>
Adrien Gallouët <adrien@gallouet.fr> <angt@users.noreply.github.com> Adrien Gallouët <adrien@gallouet.fr> <angt@users.noreply.github.com>
Ahmed Kamal <email.ahmedkamal@googlemail.com> Ahmed Kamal <email.ahmedkamal@googlemail.com>
Ahmet Alp Balkan <ahmetb@microsoft.com> <ahmetalpbalkan@gmail.com> Ahmet Alp Balkan <ahmetb@microsoft.com> <ahmetalpbalkan@gmail.com>
AJ Bowen <aj@soulshake.net> AJ Bowen <aj@gandi.net>
AJ Bowen <aj@soulshake.net> <aj@gandi.net> AJ Bowen <aj@gandi.net> <amy@gandi.net>
AJ Bowen <aj@soulshake.net> <amy@gandi.net>
Akihiro Matsushima <amatsusbit@gmail.com> <amatsus@users.noreply.github.com> Akihiro Matsushima <amatsusbit@gmail.com> <amatsus@users.noreply.github.com>
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> Akihiro Suda <suda.akihiro@lab.ntt.co.jp> <suda.kyoto@gmail.com>
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>
Aleksa Sarai <asarai@suse.de> Aleksa Sarai <asarai@suse.de>
Aleksa Sarai <asarai@suse.de> <asarai@suse.com> Aleksa Sarai <asarai@suse.de> <asarai@suse.com>
Aleksa Sarai <asarai@suse.de> <cyphar@cyphar.com> Aleksa Sarai <asarai@suse.de> <cyphar@cyphar.com>
@@ -34,17 +23,13 @@ Aleksandrs Fadins <aleks@s-ko.net>
Alessandro Boch <aboch@tetrationanalytics.com> <aboch@docker.com> Alessandro Boch <aboch@tetrationanalytics.com> <aboch@docker.com>
Alex Chen <alexchenunix@gmail.com> <root@localhost.localdomain> Alex Chen <alexchenunix@gmail.com> <root@localhost.localdomain>
Alex Ellis <alexellis2@gmail.com> Alex Ellis <alexellis2@gmail.com>
Alexander Chneerov <achneerov@gmail.com>
Alexander Larsson <alexl@redhat.com> <alexander.larsson@gmail.com> Alexander Larsson <alexl@redhat.com> <alexander.larsson@gmail.com>
Alexander Morozov <lk4d4math@gmail.com> Alexander Morozov <lk4d4@docker.com>
Alexander Morozov <lk4d4math@gmail.com> <lk4d4@docker.com> Alexander Morozov <lk4d4@docker.com> <lk4d4math@gmail.com>
Alexandre Beslic <alexandre.beslic@gmail.com> <abronan@docker.com> Alexandre Beslic <alexandre.beslic@gmail.com> <abronan@docker.com>
Alexandre Levavasseur <alexandre+oss@13x.fr>
Alexis Couvreur <alexiscouvreur.pro@gmail.com>
Alicia Lauerman <alicia@eta.im> <allydevour@me.com> Alicia Lauerman <alicia@eta.im> <allydevour@me.com>
Allen Sun <allensun.shl@alibaba-inc.com> <allen.sun@daocloud.io> Allen Sun <allensun.shl@alibaba-inc.com> <allen.sun@daocloud.io>
Allen Sun <allensun.shl@alibaba-inc.com> <shlallen1990@gmail.com> Allen Sun <allensun.shl@alibaba-inc.com> <shlallen1990@gmail.com>
Allie Sadler <allie.sadler@docker.com>
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@microsoft.com> Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@microsoft.com>
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@outlook.com> Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@outlook.com>
André Martins <aanm90@gmail.com> <martins@noironetworks.com> André Martins <aanm90@gmail.com> <martins@noironetworks.com>
@@ -60,24 +45,13 @@ Anuj Bahuguna <anujbahuguna.dev@gmail.com>
Anuj Bahuguna <anujbahuguna.dev@gmail.com> <abahuguna@fiberlink.com> Anuj Bahuguna <anujbahuguna.dev@gmail.com> <abahuguna@fiberlink.com>
Anusha Ragunathan <anusha.ragunathan@docker.com> <anusha@docker.com> Anusha Ragunathan <anusha.ragunathan@docker.com> <anusha@docker.com>
Ao Li <la9249@163.com> Ao Li <la9249@163.com>
Arko Dasgupta <arko@tetrate.io> Arnaud Porterie <arnaud.porterie@docker.com>
Arko Dasgupta <arko@tetrate.io> <arko.dasgupta@docker.com> Arnaud Porterie <arnaud.porterie@docker.com> <icecrime@gmail.com>
Arko Dasgupta <arko@tetrate.io> <arkodg@users.noreply.github.com>
Arnaud Porterie <icecrime@gmail.com>
Arnaud Porterie <icecrime@gmail.com> <arnaud.porterie@docker.com>
Arthur Flageul <arthur.flageul@gmail.com>
Arthur Flageul <arthur.flageul@gmail.com> <arthur.flageul@docker.com>
Arthur Gautier <baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net> Arthur Gautier <baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net>
Arthur Peka <arthur.peka@outlook.com> <arthrp@users.noreply.github.com>
Austin Vazquez <austin.vazquez@docker.com>
Austin Vazquez <austin.vazquez@docker.com> <55906459+austinvazquez@users.noreply.github.com>
Austin Vazquez <austin.vazquez@docker.com> <austin.vazquez.dev@gmail.com>
Austin Vazquez <austin.vazquez@docker.com> <macedonv@amazon.com>
Avi Miller <avi.miller@oracle.com> <avi.miller@gmail.com> Avi Miller <avi.miller@oracle.com> <avi.miller@gmail.com>
Ben Bonnefoy <frenchben@docker.com> Ben Bonnefoy <frenchben@docker.com>
Ben Golub <ben.golub@dotcloud.com> Ben Golub <ben.golub@dotcloud.com>
Ben Toews <mastahyeti@gmail.com> <mastahyeti@users.noreply.github.com> Ben Toews <mastahyeti@gmail.com> <mastahyeti@users.noreply.github.com>
Benjamin Nater <me@bn4t.me>
Benoit Chesneau <bchesneau@gmail.com> Benoit Chesneau <bchesneau@gmail.com>
Bhiraj Butala <abhiraj.butala@gmail.com> Bhiraj Butala <abhiraj.butala@gmail.com>
Bhumika Bayani <bhumikabayani@gmail.com> Bhumika Bayani <bhumikabayani@gmail.com>
@@ -86,21 +60,13 @@ Bill Wang <ozbillwang@gmail.com> <SydOps@users.noreply.github.com>
Bin Liu <liubin0329@gmail.com> Bin Liu <liubin0329@gmail.com>
Bin Liu <liubin0329@gmail.com> <liubin0329@users.noreply.github.com> Bin Liu <liubin0329@gmail.com> <liubin0329@users.noreply.github.com>
Bingshen Wang <bingshen.wbs@alibaba-inc.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> 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.co>
Brandon Philips <brandon.philips@coreos.com> <brandon@ifup.org> Brandon Philips <brandon.philips@coreos.com> <brandon@ifup.org>
Brent Salisbury <brent.salisbury@docker.com> <brent@docker.com> Brent Salisbury <brent.salisbury@docker.com> <brent@docker.com>
Brian Goff <cpuguy83@gmail.com> Brian Goff <cpuguy83@gmail.com>
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.home> Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.home>
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.local> 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> Chad Faragher <wyckster@hotmail.com>
Chander Govindarajan <chandergovind@gmail.com> Chander Govindarajan <chandergovind@gmail.com>
Chao Wang <wangchao.fnst@cn.fujitsu.com> <chaowang@localhost.localdomain> Chao Wang <wangchao.fnst@cn.fujitsu.com> <chaowang@localhost.localdomain>
@@ -110,19 +76,13 @@ Chen Chuanliang <chen.chuanliang@zte.com.cn>
Chen Mingjie <chenmingjie0828@163.com> Chen Mingjie <chenmingjie0828@163.com>
Chen Qiu <cheney-90@hotmail.com> Chen Qiu <cheney-90@hotmail.com>
Chen Qiu <cheney-90@hotmail.com> <21321229@zju.edu.cn> 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 Dias <cdias@microsoft.com>
Chris McKinnel <chris.mckinnel@tangentlabs.co.uk> Chris McKinnel <chris.mckinnel@tangentlabs.co.uk>
Christopher Biscardi <biscarch@sketcht.com> Christopher Biscardi <biscarch@sketcht.com>
Christopher Crone <christopher.crone@docker.com>
Christopher Latham <sudosurootdev@gmail.com> Christopher Latham <sudosurootdev@gmail.com>
Christy Norman <christy@linux.vnet.ibm.com> Christy Norman <christy@linux.vnet.ibm.com>
Chun Chen <ramichen@tencent.com> <chenchun.feed@gmail.com> Chun Chen <ramichen@tencent.com> <chenchun.feed@gmail.com>
Comical Derskeal <27731088+derskeal@users.noreply.github.com>
Corbin Coleman <corbin.coleman@docker.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>
Cristian Staretu <cristian.staretu@gmail.com> <unclejack@users.noreply.github.com> Cristian Staretu <cristian.staretu@gmail.com> <unclejack@users.noreply.github.com>
Cristian Staretu <cristian.staretu@gmail.com> <unclejacksons@gmail.com> Cristian Staretu <cristian.staretu@gmail.com> <unclejacksons@gmail.com>
@@ -130,9 +90,7 @@ CUI Wei <ghostplant@qq.com> cuiwei13 <cuiwei13@pku.edu.cn>
Daehyeok Mun <daehyeok@gmail.com> Daehyeok Mun <daehyeok@gmail.com>
Daehyeok Mun <daehyeok@gmail.com> <daehyeok@daehyeok-ui-MacBook-Air.local> Daehyeok Mun <daehyeok@gmail.com> <daehyeok@daehyeok-ui-MacBook-Air.local>
Daehyeok Mun <daehyeok@gmail.com> <daehyeok@daehyeokui-MacBook-Air.local> Daehyeok Mun <daehyeok@gmail.com> <daehyeok@daehyeokui-MacBook-Air.local>
Daisuke Ito <itodaisuke00@gmail.com>
Dan Feldman <danf@jfrog.com> Dan Feldman <danf@jfrog.com>
Danial Gharib <danial.mail.gh@gmail.com>
Daniel Dao <dqminh@cloudflare.com> Daniel Dao <dqminh@cloudflare.com>
Daniel Dao <dqminh@cloudflare.com> <dqminh89@gmail.com> Daniel Dao <dqminh@cloudflare.com> <dqminh89@gmail.com>
Daniel Garcia <daniel@danielgarcia.info> Daniel Garcia <daniel@danielgarcia.info>
@@ -152,29 +110,17 @@ Dattatraya Kumbhar <dattatraya.kumbhar@gslab.com>
Dave Goodchild <buddhamagnet@gmail.com> Dave Goodchild <buddhamagnet@gmail.com>
Dave Henderson <dhenderson@gmail.com> <Dave.Henderson@ca.ibm.com> Dave Henderson <dhenderson@gmail.com> <Dave.Henderson@ca.ibm.com>
Dave Tucker <dt@docker.com> <dave@dtucker.co.uk> Dave Tucker <dt@docker.com> <dave@dtucker.co.uk>
David Alvarez <david.alvarez@flyeralarm.com>
David Alvarez <david.alvarez@flyeralarm.com> <busilezas@gmail.com>
David Dooling <david.dooling@docker.com>
David Dooling <david.dooling@docker.com> <dooling@gmail.com>
David Karlsson <david.karlsson@docker.com>
David Karlsson <david.karlsson@docker.com> <35727626+dvdksn@users.noreply.github.com>
David M. Karr <davidmichaelkarr@gmail.com> David M. Karr <davidmichaelkarr@gmail.com>
David Sheets <dsheets@docker.com> <sheets@alum.mit.edu> David Sheets <dsheets@docker.com> <sheets@alum.mit.edu>
David Sissitka <me@dsissitka.com> David Sissitka <me@dsissitka.com>
David Williamson <david.williamson@docker.com> <davidwilliamson@users.noreply.github.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> <dsxiao@dataman-inc.com>
Deshi Xiao <dxiao@redhat.com> <xiaods@gmail.com> Deshi Xiao <dxiao@redhat.com> <xiaods@gmail.com>
Diego Siqueira <dieg0@live.com> Diego Siqueira <dieg0@live.com>
Diogo Monica <diogo@docker.com> <diogo.monica@gmail.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> Dominik Honnef <dominik@honnef.co> <dominikh@fork-bomb.org>
Doug Davis <dug@us.ibm.com> <duglin@users.noreply.github.com> Doug Davis <dug@us.ibm.com> <duglin@users.noreply.github.com>
Doug Tangren <d.tangren@gmail.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>
Elan Ruusamäe <glen@pld-linux.org> <glen@delfi.ee> Elan Ruusamäe <glen@pld-linux.org> <glen@delfi.ee>
Elango Sivanandam <elango.siva@docker.com> Elango Sivanandam <elango.siva@docker.com>
@@ -198,20 +144,14 @@ Fengtu Wang <wangfengtu@huawei.com> <wangfengtu@huawei.com>
Francisco Carriedo <fcarriedo@gmail.com> Francisco Carriedo <fcarriedo@gmail.com>
Frank Rosquin <frank.rosquin+github@gmail.com> <frank.rosquin@gmail.com> Frank Rosquin <frank.rosquin+github@gmail.com> <frank.rosquin@gmail.com>
Frederick F. Kautz IV <fkautz@redhat.com> <fkautz@alumni.cmu.edu> 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> Gabriel Nicolas Avellaneda <avellaneda.gabriel@gmail.com>
Gaetan de Villele <gdevillele@gmail.com> Gaetan de Villele <gdevillele@gmail.com>
Gang Qiao <qiaohai8866@gmail.com> <1373319223@qq.com> Gang Qiao <qiaohai8866@gmail.com> <1373319223@qq.com>
George Kontridze <george@bugsnag.com> George Kontridze <george@bugsnag.com>
Gerwim Feiken <g.feiken@tfe.nl> <gerwim@gmail.com> Gerwim Feiken <g.feiken@tfe.nl> <gerwim@gmail.com>
Giau. Tran Minh <hello@giautm.dev>
Giau. Tran Minh <hello@giautm.dev> <12751435+giautm@users.noreply.github.com>
Giampaolo Mancini <giampaolo@trampolineup.com> Giampaolo Mancini <giampaolo@trampolineup.com>
Gopikannan Venugopalsamy <gopikannan.venugopalsamy@gmail.com> Gopikannan Venugopalsamy <gopikannan.venugopalsamy@gmail.com>
Gou Rao <gou@portworx.com> <gourao@users.noreply.github.com> Gou Rao <gou@portworx.com> <gourao@users.noreply.github.com>
Graeme Wiebe <graeme.wiebe@gmail.com>
Graeme Wiebe <graeme.wiebe@gmail.com> <79593869+TheRealGramdalf@users.noreply.github.com>
Greg Stephens <greg@udon.org> Greg Stephens <greg@udon.org>
Guillaume J. Charmes <guillaume.charmes@docker.com> <charmes.guillaume@gmail.com> Guillaume J. Charmes <guillaume.charmes@docker.com> <charmes.guillaume@gmail.com>
Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume.charmes@dotcloud.com> Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume.charmes@dotcloud.com>
@@ -219,14 +159,12 @@ 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@docker.com>
Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume@dotcloud.com> Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume@dotcloud.com>
Guillaume Le Floch <glfloch@gmail.com> Guillaume Le Floch <glfloch@gmail.com>
Guillaume Tardif <guillaume.tardif@gmail.com>
Gurjeet Singh <gurjeet@singh.im> <singh.gurjeet@gmail.com> Gurjeet Singh <gurjeet@singh.im> <singh.gurjeet@gmail.com>
Gustav Sinder <gustav.sinder@gmail.com> Gustav Sinder <gustav.sinder@gmail.com>
Günther Jungbluth <gunther@gameslabs.net> Günther Jungbluth <gunther@gameslabs.net>
Hakan Özler <hakan.ozler@kodcu.com> Hakan Özler <hakan.ozler@kodcu.com>
Hao Shu Wei <haosw@cn.ibm.com> Hao Shu Wei <haosw@cn.ibm.com>
Hao Shu Wei <haosw@cn.ibm.com> <haoshuwei1989@163.com> Hao Shu Wei <haosw@cn.ibm.com> <haoshuwei1989@163.com>
Harald Albers <github@albersweb.de>
Harald Albers <github@albersweb.de> <albers@users.noreply.github.com> Harald Albers <github@albersweb.de> <albers@users.noreply.github.com>
Harold Cooper <hrldcpr@gmail.com> Harold Cooper <hrldcpr@gmail.com>
Harry Zhang <harryz@hyper.sh> <harryzhang@zju.edu.cn> Harry Zhang <harryz@hyper.sh> <harryzhang@zju.edu.cn>
@@ -239,7 +177,6 @@ Hollie Teal <hollie@docker.com>
Hollie Teal <hollie@docker.com> <hollie.teal@docker.com> Hollie Teal <hollie@docker.com> <hollie.teal@docker.com>
Hollie Teal <hollie@docker.com> <hollietealok@users.noreply.github.com> Hollie Teal <hollie@docker.com> <hollietealok@users.noreply.github.com>
Hu Keping <hukeping@huawei.com> Hu Keping <hukeping@huawei.com>
Hugo Gabriel Eyherabide <hugogabriel.eyherabide@gmail.com>
Huu Nguyen <huu@prismskylabs.com> <whoshuu@gmail.com> Huu Nguyen <huu@prismskylabs.com> <whoshuu@gmail.com>
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com> Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com>
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com> <1187766782@qq.com> Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com> <1187766782@qq.com>
@@ -253,7 +190,6 @@ Jaivish Kothari <janonymous.codevulture@gmail.com>
Jake Lambert <jake.lambert@volusion.com> Jake Lambert <jake.lambert@volusion.com>
Jake Lambert <jake.lambert@volusion.com> <32850427+jake-lambert-volusion@users.noreply.github.com> Jake Lambert <jake.lambert@volusion.com> <32850427+jake-lambert-volusion@users.noreply.github.com>
Jamie Hannaford <jamie@limetree.org> <jamie.hannaford@rackspace.com> Jamie Hannaford <jamie@limetree.org> <jamie.hannaford@rackspace.com>
Jean Lecordier <jeanlecordier@hotmail.fr>
Jean-Baptiste Barth <jeanbaptiste.barth@gmail.com> Jean-Baptiste Barth <jeanbaptiste.barth@gmail.com>
Jean-Baptiste Dalido <jeanbaptiste@appgratis.com> Jean-Baptiste Dalido <jeanbaptiste@appgratis.com>
Jean-Pierre Huynh <jean-pierre.huynh@ounet.fr> <jp@moogsoft.com> Jean-Pierre Huynh <jean-pierre.huynh@ounet.fr> <jp@moogsoft.com>
@@ -261,17 +197,14 @@ Jean-Tiare Le Bigot <jt@yadutaf.fr> <admin@jtlebi.fr>
Jeff Anderson <jeff@docker.com> <jefferya@programmerq.net> Jeff Anderson <jeff@docker.com> <jefferya@programmerq.net>
Jeff Nickoloff <jeff.nickoloff@gmail.com> <jeff@allingeek.com> Jeff Nickoloff <jeff.nickoloff@gmail.com> <jeff@allingeek.com>
Jeroen Franse <jeroenfranse@gmail.com> Jeroen Franse <jeroenfranse@gmail.com>
Jessica Frazelle <jess@oxide.computer> Jessica Frazelle <jessfraz@google.com>
Jessica Frazelle <jess@oxide.computer> <acidburn@docker.com> Jessica Frazelle <jessfraz@google.com> <acidburn@docker.com>
Jessica Frazelle <jess@oxide.computer> <acidburn@google.com> Jessica Frazelle <jessfraz@google.com> <acidburn@google.com>
Jessica Frazelle <jess@oxide.computer> <acidburn@microsoft.com> Jessica Frazelle <jessfraz@google.com> <jess@docker.com>
Jessica Frazelle <jess@oxide.computer> <jess@docker.com> Jessica Frazelle <jessfraz@google.com> <jess@mesosphere.com>
Jessica Frazelle <jess@oxide.computer> <jess@mesosphere.com> Jessica Frazelle <jessfraz@google.com> <jfrazelle@users.noreply.github.com>
Jessica Frazelle <jess@oxide.computer> <jessfraz@google.com> Jessica Frazelle <jessfraz@google.com> <me@jessfraz.com>
Jessica Frazelle <jess@oxide.computer> <jfrazelle@users.noreply.github.com> Jessica Frazelle <jessfraz@google.com> <princess@docker.com>
Jessica Frazelle <jess@oxide.computer> <me@jessfraz.com>
Jessica Frazelle <jess@oxide.computer> <princess@docker.com>
Jim Chen <njucjc@gmail.com>
Jim Galasyn <jim.galasyn@docker.com> Jim Galasyn <jim.galasyn@docker.com>
Jiuyue Ma <majiuyue@huawei.com> Jiuyue Ma <majiuyue@huawei.com>
Joey Geiger <jgeiger@gmail.com> Joey Geiger <jgeiger@gmail.com>
@@ -280,21 +213,16 @@ Joffrey F <joffrey@docker.com> <f.joffrey@gmail.com>
Joffrey F <joffrey@docker.com> <joffrey@dotcloud.com> Joffrey F <joffrey@docker.com> <joffrey@dotcloud.com>
Johan Euphrosine <proppy@google.com> <proppy@aminche.com> Johan Euphrosine <proppy@google.com> <proppy@aminche.com>
John Harris <john@johnharris.io> John Harris <john@johnharris.io>
John Howard <github@lowenna.com> John Howard (VM) <John.Howard@microsoft.com>
John Howard <github@lowenna.com> <jhoward@microsoft.com> John Howard (VM) <John.Howard@microsoft.com> <jhoward@microsoft.com>
John Howard <github@lowenna.com> <jhoward@ntdev.microsoft.com> John Howard (VM) <John.Howard@microsoft.com> <jhoward@ntdev.microsoft.com>
John Howard <github@lowenna.com> <jhowardmsft@users.noreply.github.com> John Howard (VM) <John.Howard@microsoft.com> <jhowardmsft@users.noreply.github.com>
John Howard <github@lowenna.com> <John.Howard@microsoft.com> John Howard (VM) <John.Howard@microsoft.com> <john.howard@microsoft.com>
John Howard <github@lowenna.com> <john.howard@microsoft.com>
John Stephens <johnstep@docker.com> <johnstep@users.noreply.github.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 Arentsen <blissdev@gmail.com>
Jordan Jennings <jjn2009@gmail.com> <jjn2009@users.noreply.github.com> Jordan Jennings <jjn2009@gmail.com> <jjn2009@users.noreply.github.com>
Jorit Kleine-Möllhoff <joppich@bricknet.de> <joppich@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 <jose@seatgeek.com> <josegonzalez@users.noreply.github.com>
Jose Diaz-Gonzalez <email@josediazgonzalez.com> <jose@seatgeek.com>
Jose Diaz-Gonzalez <email@josediazgonzalez.com> <josegonzalez@users.noreply.github.com>
Josh Eveleth <joshe@opendns.com> <jeveleth@users.noreply.github.com> Josh Eveleth <joshe@opendns.com> <jeveleth@users.noreply.github.com>
Josh Hawn <josh.hawn@docker.com> <jlhawn@berkeley.edu> Josh Hawn <josh.hawn@docker.com> <jlhawn@berkeley.edu>
Josh Horwitz <horwitz@addthis.com> <horwitzja@gmail.com> Josh Horwitz <horwitz@addthis.com> <horwitzja@gmail.com>
@@ -316,20 +244,12 @@ Kai Qiang Wu (Kennan) <wkq5325@gmail.com> <wkqwu@cn.ibm.com>
Kamil Domański <kamil@domanski.co> Kamil Domański <kamil@domanski.co>
Kamjar Gerami <kami.gerami@gmail.com> Kamjar Gerami <kami.gerami@gmail.com>
Kat Samperi <kat.samperi@gmail.com> <kizzie@users.noreply.github.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 Cochrane <kencochrane@gmail.com> <KenCochrane@gmail.com>
Ken Herner <kherner@progress.com> <chosenken@gmail.com> Ken Herner <kherner@progress.com> <chosenken@gmail.com>
Kenfe-Mickaël Laventure <mickael.laventure@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 Feyrer <kevin.feyrer@btinternet.com> <kevinfeyrer@users.noreply.github.com>
Kevin Kern <kaiwentan@harmonycloud.cn> Kevin Kern <kaiwentan@harmonycloud.cn>
Kevin Meredith <kevin.m.meredith@gmail.com> Kevin Meredith <kevin.m.meredith@gmail.com>
Kevin Woblick <mail@kovah.de>
Kir Kolyshkin <kolyshkin@gmail.com> Kir Kolyshkin <kolyshkin@gmail.com>
Kir Kolyshkin <kolyshkin@gmail.com> <kir@openvz.org> Kir Kolyshkin <kolyshkin@gmail.com> <kir@openvz.org>
Kir Kolyshkin <kolyshkin@gmail.com> <kolyshkin@users.noreply.github.com> Kir Kolyshkin <kolyshkin@gmail.com> <kolyshkin@users.noreply.github.com>
@@ -338,13 +258,11 @@ Konstantin Gribov <grossws@gmail.com>
Konstantin Pelykh <kpelykh@zettaset.com> Konstantin Pelykh <kpelykh@zettaset.com>
Kotaro Yoshimatsu <kotaro.yoshimatsu@gmail.com> Kotaro Yoshimatsu <kotaro.yoshimatsu@gmail.com>
Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp> <kunal.kushwaha@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> Lajos Papp <lajos.papp@sequenceiq.com> <lalyos@yahoo.com>
Lei Jitang <leijitang@huawei.com> Lei Jitang <leijitang@huawei.com>
Lei Jitang <leijitang@huawei.com> <leijitang@gmail.com> Lei Jitang <leijitang@huawei.com> <leijitang@gmail.com>
Li Fu Bang <lifubang@acmcoder.com>
Li Yi <denverdino@gmail.com>
Li Yi <denverdino@gmail.com> <weiyuan.yl@alibaba-inc.com>
Liang Mingqiang <mqliang.zju@gmail.com> Liang Mingqiang <mqliang.zju@gmail.com>
Liang-Chi Hsieh <viirya@gmail.com> Liang-Chi Hsieh <viirya@gmail.com>
Liao Qingwei <liaoqingwei@huawei.com> Liao Qingwei <liaoqingwei@huawei.com>
@@ -354,17 +272,12 @@ Lokesh Mandvekar <lsm5@fedoraproject.org> <lsm5@redhat.com>
Lorenzo Fontana <lo@linux.com> <fontanalorenzo@me.com> Lorenzo Fontana <lo@linux.com> <fontanalorenzo@me.com>
Louis Opter <kalessin@kalessin.fr> Louis Opter <kalessin@kalessin.fr>
Louis Opter <kalessin@kalessin.fr> <louis@dotcloud.com> Louis Opter <kalessin@kalessin.fr> <louis@dotcloud.com>
Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
Luca Favatella <luca.favatella@erlang-solutions.com> <lucafavatella@users.noreply.github.com> Luca Favatella <luca.favatella@erlang-solutions.com> <lucafavatella@users.noreply.github.com>
Lukas Michael <lukas.23022005@gmail.com>
Luke Marsden <me@lukemarsden.net> <luke@digital-crocus.com> Luke Marsden <me@lukemarsden.net> <luke@digital-crocus.com>
Luo Jiyin <luojiyin@hotmail.com>
Lyn <energylyn@zju.edu.cn> Lyn <energylyn@zju.edu.cn>
Lynda O'Leary <lyndaoleary29@gmail.com> Lynda O'Leary <lyndaoleary29@gmail.com>
Lynda O'Leary <lyndaoleary29@gmail.com> <lyndaoleary@hotmail.com> Lynda O'Leary <lyndaoleary29@gmail.com> <lyndaoleary@hotmail.com>
Ma Müller <mueller-ma@users.noreply.github.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> Madhan Raj Mookkandy <MadhanRaj.Mookkandy@microsoft.com> <madhanm@microsoft.com>
Madhu Venugopal <madhu@socketplane.io> <madhu@docker.com> Madhu Venugopal <madhu@socketplane.io> <madhu@docker.com>
Mageee <fangpuyi@foxmail.com> <21521230.zju.edu.cn> Mageee <fangpuyi@foxmail.com> <21521230.zju.edu.cn>
@@ -372,10 +285,8 @@ Mansi Nahar <mmn4185@rit.edu> <mansi.nahar@macbookpro-mansinahar.local>
Mansi Nahar <mmn4185@rit.edu> <mansinahar@users.noreply.github.com> Mansi Nahar <mmn4185@rit.edu> <mansinahar@users.noreply.github.com>
Marc Abramowitz <marc@marc-abramowitz.com> <msabramo@gmail.com> Marc Abramowitz <marc@marc-abramowitz.com> <msabramo@gmail.com>
Marcelo Horacio Fortino <info@fortinux.com> <fortinux@users.noreply.github.com> Marcelo Horacio Fortino <info@fortinux.com> <fortinux@users.noreply.github.com>
Marco Spiess <marco.spiess@hotmail.de>
Marcus Linke <marcus.linke@gmx.de> Marcus Linke <marcus.linke@gmx.de>
Marianna Tessel <mtesselh@gmail.com> Marianna Tessel <mtesselh@gmail.com>
Marius Ileana <marius.ileana@gmail.com>
Mark Oates <fl0yd@me.com> Mark Oates <fl0yd@me.com>
Markan Patel <mpatel678@gmail.com> Markan Patel <mpatel678@gmail.com>
Markus Kortlang <hyp3rdino@googlemail.com> <markus.kortlang@lhsystems.com> Markus Kortlang <hyp3rdino@googlemail.com> <markus.kortlang@lhsystems.com>
@@ -395,31 +306,23 @@ Matthew Mosesohn <raytrac3r@gmail.com>
Matthew Mueller <mattmuelle@gmail.com> Matthew Mueller <mattmuelle@gmail.com>
Matthias Kühnle <git.nivoc@neverbox.com> <kuehnle@online.de> Matthias Kühnle <git.nivoc@neverbox.com> <kuehnle@online.de>
Mauricio Garavaglia <mauricio@medallia.com> <mauriciogaravaglia@gmail.com> Mauricio Garavaglia <mauricio@medallia.com> <mauriciogaravaglia@gmail.com>
Michael Crosby <crosbymichael@gmail.com> Michael Crosby <michael@docker.com> <crosby.michael@gmail.com>
Michael Crosby <crosbymichael@gmail.com> <crosby.michael@gmail.com> Michael Crosby <michael@docker.com> <crosbymichael@gmail.com>
Michael Crosby <crosbymichael@gmail.com> <michael@crosbymichael.com> Michael Crosby <michael@docker.com> <michael@crosbymichael.com>
Michael Crosby <crosbymichael@gmail.com> <michael@docker.com>
Michael Crosby <crosbymichael@gmail.com> <michael@thepasture.io>
Michael Hudson-Doyle <michael.hudson@canonical.com> <michael.hudson@linaro.org> Michael Hudson-Doyle <michael.hudson@canonical.com> <michael.hudson@linaro.org>
Michael Huettermann <michael@huettermann.net> Michael Huettermann <michael@huettermann.net>
Michael Käufl <docker@c.michael-kaeufl.de> <michael-k@users.noreply.github.com> Michael Käufl <docker@c.michael-kaeufl.de> <michael-k@users.noreply.github.com>
Michael Spetsiotis <michael_spets@hotmail.com> Michael Spetsiotis <michael_spets@hotmail.com>
Michal Minář <miminar@redhat.com> Michal Minář <miminar@redhat.com>
Mickael Emirkanian <mickael.emirkanian@docker.com>
Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com> <30386061+doncicuto@users.noreply.github.com> Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com> <30386061+doncicuto@users.noreply.github.com>
Miguel Angel Fernández <elmendalerenda@gmail.com> Miguel Angel Fernández <elmendalerenda@gmail.com>
Mihai Borobocea <MihaiBorob@gmail.com> <MihaiBorobocea@gmail.com> Mihai Borobocea <MihaiBorob@gmail.com> <MihaiBorobocea@gmail.com>
Mike Casas <mkcsas0@gmail.com> <mikecasas@users.noreply.github.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> Mike Goelzer <mike.goelzer@docker.com> <mgoelzer@docker.com>
Milind Chawre <milindchawre@gmail.com> Milind Chawre <milindchawre@gmail.com>
Misty Stanley-Jones <misty@docker.com> <misty@apache.org> Misty Stanley-Jones <misty@docker.com> <misty@apache.org>
Mohammad Hossein <mhm98035@gmail.com>
Mohit Soni <mosoni@ebay.com> <mohitsoni1989@gmail.com> Mohit Soni <mosoni@ebay.com> <mohitsoni1989@gmail.com>
Moorthy RS <rsmoorthy@gmail.com> <rsmoorthy@users.noreply.github.com> Moorthy RS <rsmoorthy@gmail.com> <rsmoorthy@users.noreply.github.com>
Morten Hekkvang <morten.hekkvang@sbab.se>
Morten Hekkvang <morten.hekkvang@sbab.se> <morten.hekkvang@tele2.com>
Moysés Borges <moysesb@gmail.com> Moysés Borges <moysesb@gmail.com>
Moysés Borges <moysesb@gmail.com> <moyses.furtado@wplex.com.br> Moysés Borges <moysesb@gmail.com> <moyses.furtado@wplex.com.br>
Nace Oroz <orkica@gmail.com> Nace Oroz <orkica@gmail.com>
@@ -427,61 +330,42 @@ Nathan LeClaire <nathan.leclaire@docker.com> <nathan.leclaire@gmail.com>
Nathan LeClaire <nathan.leclaire@docker.com> <nathanleclaire@gmail.com> Nathan LeClaire <nathan.leclaire@docker.com> <nathanleclaire@gmail.com>
Neil Horman <nhorman@tuxdriver.com> <nhorman@hmswarspite.think-freely.org> Neil Horman <nhorman@tuxdriver.com> <nhorman@hmswarspite.think-freely.org>
Nick Russo <nicholasjamesrusso@gmail.com> <nicholasrusso@icloud.com> 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 Borboën <ponsfrilus@gmail.com> <ponsfrilus@users.noreply.github.com>
Nicolas De Loof <nicolas.deloof@gmail.com>
Nigel Poulton <nigelpoulton@hotmail.com> Nigel Poulton <nigelpoulton@hotmail.com>
Nik Nyby <nikolas@gnu.org> <nnyby@columbia.edu> Nik Nyby <nikolas@gnu.org> <nnyby@columbia.edu>
Nolan Darilek <nolan@thewordnerd.info> Nolan Darilek <nolan@thewordnerd.info>
O.S. Tezer <ostezer@gmail.com> O.S. Tezer <ostezer@gmail.com>
O.S. Tezer <ostezer@gmail.com> <ostezer@users.noreply.github.com> O.S. Tezer <ostezer@gmail.com> <ostezer@users.noreply.github.com>
Oh Jinkyun <tintypemolly@gmail.com> <tintypemolly@Ohui-MacBook-Pro.local> Oh Jinkyun <tintypemolly@gmail.com> <tintypemolly@Ohui-MacBook-Pro.local>
Oliver Pomeroy <oppomeroy@gmail.com>
Ouyang Liduo <oyld0210@163.com> Ouyang Liduo <oyld0210@163.com>
Patrick St. laurent <patrick@saint-laurent.us>
Patrick Stapleton <github@gdi2290.com> Patrick Stapleton <github@gdi2290.com>
Paul Liljenberg <liljenberg.paul@gmail.com> <letters@paulnotcom.se> Paul Liljenberg <liljenberg.paul@gmail.com> <letters@paulnotcom.se>
Pavel Tikhomirov <ptikhomirov@virtuozzo.com> <ptikhomirov@parallels.com> Pavel Tikhomirov <ptikhomirov@virtuozzo.com> <ptikhomirov@parallels.com>
Pawel Konczalski <mail@konczalski.de> 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 Choi <phkchoi89@gmail.com> <reikani@Peters-MacBook-Pro.local>
Peter Dave Hello <hsu@peterdavehello.org> <PeterDaveHello@users.noreply.github.com> Peter Dave Hello <hsu@peterdavehello.org> <PeterDaveHello@users.noreply.github.com>
Peter Hsu <shhsu@microsoft.com> Peter Hsu <shhsu@microsoft.com>
Peter Jaffe <pjaffe@nevo.com> Peter Jaffe <pjaffe@nevo.com>
Peter Nagy <xificurC@gmail.com> <pnagy@gratex.com> Peter Nagy <xificurC@gmail.com> <pnagy@gratex.com>
Peter Waller <p@pwaller.net> <peter@scraperwiki.com> Peter Waller <p@pwaller.net> <peter@scraperwiki.com>
Phil Estes <estesp@gmail.com> Phil Estes <estesp@linux.vnet.ibm.com> <estesp@gmail.com>
Phil Estes <estesp@gmail.com> <estesp@amazon.com>
Phil Estes <estesp@gmail.com> <estesp@linux.vnet.ibm.com>
Philip Alexander Etling <paetling@gmail.com> Philip Alexander Etling <paetling@gmail.com>
Philipp Gillé <philipp.gille@gmail.com> <philippgille@users.noreply.github.com> Philipp Gillé <philipp.gille@gmail.com> <philippgille@users.noreply.github.com>
Qiang Huang <h.huangqiang@huawei.com> Qiang Huang <h.huangqiang@huawei.com>
Qiang Huang <h.huangqiang@huawei.com> <qhuang@10.0.2.15> Qiang Huang <h.huangqiang@huawei.com> <qhuang@10.0.2.15>
Ray Tsang <rayt@google.com> <saturnism@users.noreply.github.com> Ray Tsang <rayt@google.com> <saturnism@users.noreply.github.com>
Renaud Gaubert <rgaubert@nvidia.com> <renaud.gaubert@gmail.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> Robert Terhaar <rterhaar@atlanticdynamic.com> <robbyt@users.noreply.github.com>
Roberto G. Hashioka <roberto.hashioka@docker.com> <roberto_hashioka@hotmail.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> 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> Roman Dudin <katrmr@gmail.com> <decadent@users.noreply.github.com>
Ross Boucher <rboucher@gmail.com> Ross Boucher <rboucher@gmail.com>
Rui JingAn <quiterace@gmail.com>
Runshen Zhu <runshen.zhu@gmail.com> Runshen Zhu <runshen.zhu@gmail.com>
Ryan Stelly <ryan.stelly@live.com> Ryan Stelly <ryan.stelly@live.com>
Sakeven Jiang <jc5930@sina.cn> Sakeven Jiang <jc5930@sina.cn>
Samarth Shah <samashah@microsoft.com>
Sandeep Bansal <sabansal@microsoft.com> Sandeep Bansal <sabansal@microsoft.com>
Sandeep Bansal <sabansal@microsoft.com> <msabansal@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> Sargun Dhillon <sargun@netflix.com> <sargun@sargun.me>
Saurabh Kumar <saurabhkumar0184@gmail.com>
Sean Lee <seanlee@tw.ibm.com> <scaleoutsean@users.noreply.github.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> <sebastiaan@ws-key-sebas3.dpi1.dpi>
Sebastiaan van Stijn <github@gone.nl> <thaJeztah@users.noreply.github.com> Sebastiaan van Stijn <github@gone.nl> <thaJeztah@users.noreply.github.com>
@@ -503,7 +387,6 @@ Solomon Hykes <solomon@docker.com> <solomon.hykes@dotcloud.com>
Solomon Hykes <solomon@docker.com> <solomon@dotcloud.com> Solomon Hykes <solomon@docker.com> <solomon@dotcloud.com>
Soshi Katsuta <soshi.katsuta@gmail.com> Soshi Katsuta <soshi.katsuta@gmail.com>
Soshi Katsuta <soshi.katsuta@gmail.com> <katsuta_soshi@cyberagent.co.jp> 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>
Sridhar Ratnakumar <sridharr@activestate.com> <github@srid.name> Sridhar Ratnakumar <sridharr@activestate.com> <github@srid.name>
Srini Brahmaroutu <srbrahma@us.ibm.com> <sbrahma@us.ibm.com> Srini Brahmaroutu <srbrahma@us.ibm.com> <sbrahma@us.ibm.com>
@@ -519,7 +402,6 @@ Stephen Day <stevvooe@gmail.com> <stephen.day@docker.com>
Stephen Day <stevvooe@gmail.com> <stevvooe@users.noreply.github.com> Stephen Day <stevvooe@gmail.com> <stevvooe@users.noreply.github.com>
Steve Desmond <steve@vtsv.ca> <stevedesmond-ca@users.noreply.github.com> Steve Desmond <steve@vtsv.ca> <stevedesmond-ca@users.noreply.github.com>
Steve Richards <steve.richards@docker.com> stevejr <> Steve Richards <steve.richards@docker.com> stevejr <>
Stuart Williams <pid@pidster.com>
Sun Gengze <690388648@qq.com> Sun Gengze <690388648@qq.com>
Sun Jianbo <wonderflow.sun@gmail.com> Sun Jianbo <wonderflow.sun@gmail.com>
Sun Jianbo <wonderflow.sun@gmail.com> <wonderflow@zju.edu.cn> Sun Jianbo <wonderflow.sun@gmail.com> <wonderflow@zju.edu.cn>
@@ -534,11 +416,8 @@ Sven Dowideit <SvenDowideit@home.org.au> <SvenDowideit@users.noreply.github.com>
Sven Dowideit <SvenDowideit@home.org.au> <¨SvenDowideit@home.org.au¨> Sven Dowideit <SvenDowideit@home.org.au> <¨SvenDowideit@home.org.au¨>
Sylvain Bellemare <sylvain@ascribe.io> Sylvain Bellemare <sylvain@ascribe.io>
Sylvain Bellemare <sylvain@ascribe.io> <sylvain.bellemare@ezeep.com> Sylvain Bellemare <sylvain@ascribe.io> <sylvain.bellemare@ezeep.com>
Takeshi Koenuma <t.koenuma2@gmail.com>
Tangi Colin <tangicolin@gmail.com> Tangi Colin <tangicolin@gmail.com>
Teiva Harsanyi <t.harsanyi@thebeat.co>
Tejesh Mehta <tejesh.mehta@gmail.com> <tj@init.me> 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 Peskens <thatcher@docker.com> <thatcher@dotcloud.com> Thatcher Peskens <thatcher@docker.com> <thatcher@dotcloud.com>
Thatcher Peskens <thatcher@docker.com> <thatcher@gmx.net> Thatcher Peskens <thatcher@docker.com> <thatcher@gmx.net>
@@ -554,8 +433,6 @@ Tim Bart <tim@fewagainstmany.com>
Tim Bosse <taim@bosboot.org> <maztaim@users.noreply.github.com> Tim Bosse <taim@bosboot.org> <maztaim@users.noreply.github.com>
Tim Ruffles <oi@truffles.me.uk> <timruffles@googlemail.com> Tim Ruffles <oi@truffles.me.uk> <timruffles@googlemail.com>
Tim Terhorst <mynamewastaken+git@gmail.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> Tim Zju <21651152@zju.edu.cn>
Timothy Hobbs <timothyhobbs@seznam.cz> Timothy Hobbs <timothyhobbs@seznam.cz>
Toli Kuznets <toli@docker.com> Toli Kuznets <toli@docker.com>
@@ -563,20 +440,12 @@ Tom Barlow <tomwbarlow@gmail.com>
Tom Milligan <code@tommilligan.net> Tom Milligan <code@tommilligan.net>
Tom Milligan <code@tommilligan.net> <tommilligan@users.noreply.github.com> Tom Milligan <code@tommilligan.net> <tommilligan@users.noreply.github.com>
Tom Sweeney <tsweeney@redhat.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> Tõnis Tiigi <tonistiigi@gmail.com>
Trishna Guha <trishnaguha17@gmail.com> Trishna Guha <trishnaguha17@gmail.com>
Tristan Carel <tristan@cogniteev.com> Tristan Carel <tristan@cogniteev.com>
Tristan Carel <tristan@cogniteev.com> <tristan.carel@gmail.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>
Umesh Yadav <umesh4257@gmail.com> <dungeonmaster18@users.noreply.github.com> Umesh Yadav <umesh4257@gmail.com> <dungeonmaster18@users.noreply.github.com>
Varun Hotani <varunhotani@gmail.com>
Vibhu Anan <vibhuanand@outlook.com>
Victor Lyuboslavsky <victor@victoreda.com> Victor Lyuboslavsky <victor@victoreda.com>
Victor Vieux <victor.vieux@docker.com> <dev@vvieux.com> Victor Vieux <victor.vieux@docker.com> <dev@vvieux.com>
Victor Vieux <victor.vieux@docker.com> <victor.vieux@dotcloud.com> Victor Vieux <victor.vieux@docker.com> <victor.vieux@dotcloud.com>
@@ -633,4 +502,3 @@ Zhou Hao <zhouhao@cn.fujitsu.com>
Zhoulin Xie <zhoulin.xie@daocloud.io> Zhoulin Xie <zhoulin.xie@daocloud.io>
Zhu Kunjia <zhu.kunjia@zte.com.cn> Zhu Kunjia <zhu.kunjia@zte.com.cn>
Zou Yu <zouyu7@huawei.com> Zou Yu <zouyu7@huawei.com>
Александр Менщиков <__Singleton__@hackerdom.ru>
+20 -278
View File
@@ -1,80 +1,53 @@
# File @generated by scripts/docs/generate-authors.sh. DO NOT EDIT. # This file lists all individuals having contributed content to the repository.
# This file lists all contributors to the repository. # For how it is generated, see `scripts/docs/generate-authors.sh`.
# See scripts/docs/generate-authors.sh to make modifications.
4RH1T3CT0R7 <iprintercanon@gmail.com>
A. Lester Buck III <github-reg@nbolt.com>
Aanand Prasad <aanand.prasad@gmail.com> Aanand Prasad <aanand.prasad@gmail.com>
Aaron L. Xu <liker.xu@foxmail.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> Aaron.L.Xu <likexu@harmonycloud.cn>
Abdur Rehman <abdur_rehman@mentor.com> Abdur Rehman <abdur_rehman@mentor.com>
Abhinandan Prativadi <abhi@docker.com> Abhinandan Prativadi <abhi@docker.com>
Abin Shahab <ashahab@altiscale.com> Abin Shahab <ashahab@altiscale.com>
Abreto FU <public@abreto.email>
Ace Tang <aceapril@126.com> Ace Tang <aceapril@126.com>
Addam Hardy <addam.hardy@gmail.com> Addam Hardy <addam.hardy@gmail.com>
Adolfo Ochagavía <aochagavia92@gmail.com> Adolfo Ochagavía <aochagavia92@gmail.com>
Adrian Plata <adrian.plata@docker.com>
Adrien Duermael <adrien@duermael.com> Adrien Duermael <adrien@duermael.com>
Adrien Folie <folie.adrien@gmail.com> Adrien Folie <folie.adrien@gmail.com>
Adyanth Hosavalike <ahosavalike@ucsd.edu>
Ahmet Alp Balkan <ahmetb@microsoft.com> Ahmet Alp Balkan <ahmetb@microsoft.com>
Aidan Feldman <aidan.feldman@gmail.com> Aidan Feldman <aidan.feldman@gmail.com>
Aidan Hobson Sayers <aidanhs@cantab.net> Aidan Hobson Sayers <aidanhs@cantab.net>
AJ Bowen <aj@soulshake.net> AJ Bowen <aj@gandi.net>
Akhil Mohan <akhil.mohan@mayadata.io> Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
Akim Demaille <akim.demaille@docker.com> Akim Demaille <akim.demaille@docker.com>
Alan Thompson <cloojure@gmail.com> Alan Thompson <cloojure@gmail.com>
Alano Terblanche <alano.terblanche@docker.com>
Albert Callarisa <shark234@gmail.com> Albert Callarisa <shark234@gmail.com>
Alberto Roura <mail@albertoroura.com>
Albin Kerouanton <albinker@gmail.com>
Aleksa Sarai <asarai@suse.de> Aleksa Sarai <asarai@suse.de>
Aleksander Piotrowski <apiotrowski312@gmail.com>
Alessandro Boch <aboch@tetrationanalytics.com> Alessandro Boch <aboch@tetrationanalytics.com>
Alex Couture-Beil <alex@earthly.dev>
Alex Mavrogiannis <alex.mavrogiannis@docker.com> Alex Mavrogiannis <alex.mavrogiannis@docker.com>
Alex Mayer <amayer5125@gmail.com> Alex Mayer <amayer5125@gmail.com>
Alexander Boyd <alex@opengroove.org> Alexander Boyd <alex@opengroove.org>
Alexander Chneerov <achneerov@gmail.com>
Alexander Larsson <alexl@redhat.com> Alexander Larsson <alexl@redhat.com>
Alexander Morozov <lk4d4math@gmail.com> Alexander Morozov <lk4d4@docker.com>
Alexander Ryabov <i@sepa.spb.ru> Alexander Ryabov <i@sepa.spb.ru>
Alexandre González <agonzalezro@gmail.com> Alexandre González <agonzalezro@gmail.com>
Alexandre Levavasseur <alexandre+oss@13x.fr>
Alexandre Vallières-Lagacé <alexandre.valliereslagace@docker.com>
Alexey Igrychev <alexey.igrychev@flant.com>
Alexis Couvreur <alexiscouvreur.pro@gmail.com>
Alfred Landrum <alfred.landrum@docker.com> Alfred Landrum <alfred.landrum@docker.com>
Ali Rostami <rostami.ali@gmail.com>
Alicia Lauerman <alicia@eta.im> Alicia Lauerman <alicia@eta.im>
Allen Sun <allensun.shl@alibaba-inc.com> Allen Sun <allensun.shl@alibaba-inc.com>
Allie Sadler <allie.sadler@docker.com>
Alvin Deng <alvin.q.deng@utexas.edu> Alvin Deng <alvin.q.deng@utexas.edu>
Amen Belayneh <amenbelayneh@gmail.com> Amen Belayneh <amenbelayneh@gmail.com>
Amey Shrivastava <72866602+AmeyShrivastava@users.noreply.github.com>
Amir Goldstein <amir73il@aquasec.com> Amir Goldstein <amir73il@aquasec.com>
Amit Krishnan <amit.krishnan@oracle.com> Amit Krishnan <amit.krishnan@oracle.com>
Amit Shukla <amit.shukla@docker.com> Amit Shukla <amit.shukla@docker.com>
Amy Lindburg <amy.lindburg@docker.com> Amy Lindburg <amy.lindburg@docker.com>
Anca Iordache <anca.iordache@docker.com>
Anda Xu <anda.xu@docker.com> Anda Xu <anda.xu@docker.com>
Andrea Luzzardi <aluzzardi@gmail.com> Andrea Luzzardi <aluzzardi@gmail.com>
Andreas Köhler <andi5.py@gmx.net> 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 France <andrew@avito.co.uk>
Andrew He <he.andrew.mail@gmail.com>
Andrew Hopp <andrew.hopp@me.com>
Andrew Hsu <andrewhsu@docker.com> Andrew Hsu <andrewhsu@docker.com>
Andrew Macpherson <hopscotch23@gmail.com> Andrew Macpherson <hopscotch23@gmail.com>
Andrew McDonnell <bugs@andrewmcdonnell.net> Andrew McDonnell <bugs@andrewmcdonnell.net>
Andrew Po <absourd.noise@gmail.com> Andrew Po <absourd.noise@gmail.com>
Andrew-Zipperer <atzipperer@gmail.com>
Andrey Petrov <andrey.petrov@shazow.net> Andrey Petrov <andrey.petrov@shazow.net>
Andrii Berehuliak <berkusandrew@gmail.com>
André Martins <aanm90@gmail.com> André Martins <aanm90@gmail.com>
Andy Goldstein <agoldste@redhat.com> Andy Goldstein <agoldste@redhat.com>
Andy Rothfusz <github@developersupport.net> Andy Rothfusz <github@developersupport.net>
@@ -87,40 +60,25 @@ Antonis Kalipetis <akalipetis@gmail.com>
Anusha Ragunathan <anusha.ragunathan@docker.com> Anusha Ragunathan <anusha.ragunathan@docker.com>
Ao Li <la9249@163.com> Ao Li <la9249@163.com>
Arash Deshmeh <adeshmeh@ca.ibm.com> Arash Deshmeh <adeshmeh@ca.ibm.com>
Archimedes Trajano <developer@trajano.net> Arnaud Porterie <arnaud.porterie@docker.com>
Arko Dasgupta <arko@tetrate.io>
Arnaud Porterie <icecrime@gmail.com>
Arnaud Rebillout <elboulangero@gmail.com>
Arthur Flageul <arthur.flageul@gmail.com>
Arthur Peka <arthur.peka@outlook.com>
Ashly Mathew <ashly.mathew@sap.com>
Ashwini Oruganti <ashwini.oruganti@gmail.com> Ashwini Oruganti <ashwini.oruganti@gmail.com>
Aslam Ahemad <aslamahemad@gmail.com>
Austin Vazquez <austin.vazquez@docker.com>
Azat Khuyiyakhmetov <shadow_uz@mail.ru> Azat Khuyiyakhmetov <shadow_uz@mail.ru>
Bardia Keyoumarsi <bkeyouma@ucsc.edu> Bardia Keyoumarsi <bkeyouma@ucsc.edu>
Barnaby Gray <barnaby@pickle.me.uk> Barnaby Gray <barnaby@pickle.me.uk>
Bastiaan Bakker <bbakker@xebia.com> Bastiaan Bakker <bbakker@xebia.com>
BastianHofmann <bastianhofmann@me.com> BastianHofmann <bastianhofmann@me.com>
Ben Bodenmiller <bbodenmiller@gmail.com>
Ben Bonnefoy <frenchben@docker.com> Ben Bonnefoy <frenchben@docker.com>
Ben Creasy <ben@bencreasy.com> Ben Creasy <ben@bencreasy.com>
Ben Firshman <ben@firshman.co.uk> Ben Firshman <ben@firshman.co.uk>
Benjamin Boudreau <boudreau.benjamin@gmail.com> Benjamin Boudreau <boudreau.benjamin@gmail.com>
Benjamin Böhmke <benjamin@boehmke.net>
Benjamin Nater <me@bn4t.me>
Benoit Sigoure <tsunanet@gmail.com> Benoit Sigoure <tsunanet@gmail.com>
Bhumika Bayani <bhumikabayani@gmail.com> Bhumika Bayani <bhumikabayani@gmail.com>
Bill Wang <ozbillwang@gmail.com> Bill Wang <ozbillwang@gmail.com>
Bin Liu <liubin0329@gmail.com> Bin Liu <liubin0329@gmail.com>
Bingshen Wang <bingshen.wbs@alibaba-inc.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> Boaz Shuster <ripcurld.github@gmail.com>
Boban Acimovic <boban.acimovic@gmail.com>
Bogdan Anton <contact@bogdananton.ro> Bogdan Anton <contact@bogdananton.ro>
Boris Pruessmann <boris@pruessmann.org> Boris Pruessmann <boris@pruessmann.org>
Brad Baker <brad@brad.fi>
Bradley Cicenas <bradley.cicenas@gmail.com> Bradley Cicenas <bradley.cicenas@gmail.com>
Brandon Mitchell <git@bmitch.net> Brandon Mitchell <git@bmitch.net>
Brandon Philips <brandon.philips@coreos.com> Brandon Philips <brandon.philips@coreos.com>
@@ -128,26 +86,16 @@ Brent Salisbury <brent.salisbury@docker.com>
Bret Fisher <bret@bretfisher.com> Bret Fisher <bret@bretfisher.com>
Brian (bex) Exelbierd <bexelbie@redhat.com> Brian (bex) Exelbierd <bexelbie@redhat.com>
Brian Goff <cpuguy83@gmail.com> Brian Goff <cpuguy83@gmail.com>
Brian Tracy <brian.tracy33@gmail.com>
Brian Wieder <brian@4wieders.com>
Bruno Sousa <bruno.sousa@docker.com>
Bruno Verachten <gounthar@gmail.com>
Bryan Bess <squarejaw@bsbess.com> Bryan Bess <squarejaw@bsbess.com>
Bryan Boreham <bjboreham@gmail.com> Bryan Boreham <bjboreham@gmail.com>
Bryan Murphy <bmurphy1976@gmail.com> Bryan Murphy <bmurphy1976@gmail.com>
bryfry <bryon.fryer@gmail.com> bryfry <bryon.fryer@gmail.com>
Calvin Liu <flycalvin@qq.com>
Cameron Spear <cameronspear@gmail.com> Cameron Spear <cameronspear@gmail.com>
Cao Weiwei <cao.weiwei30@zte.com.cn> Cao Weiwei <cao.weiwei30@zte.com.cn>
Carlo Mion <mion00@gmail.com> Carlo Mion <mion00@gmail.com>
Carlos Alexandro Becker <caarlos0@gmail.com> Carlos Alexandro Becker <caarlos0@gmail.com>
Carlos de Paula <me@carlosedp.com>
carsontham <carsontham@outlook.com>
Carston Schilds <Carston.Schilds@visier.com>
Casey Korver <casey@korver.dev>
Ce Gao <ce.gao@outlook.com> Ce Gao <ce.gao@outlook.com>
Cedric Davies <cedricda@microsoft.com> Cedric Davies <cedricda@microsoft.com>
Cesar Talledo <cesar.talledo@docker.com>
Cezar Sa Espinola <cezarsa@gmail.com> Cezar Sa Espinola <cezarsa@gmail.com>
Chad Faragher <wyckster@hotmail.com> Chad Faragher <wyckster@hotmail.com>
Chao Wang <wangchao.fnst@cn.fujitsu.com> Chao Wang <wangchao.fnst@cn.fujitsu.com>
@@ -155,21 +103,15 @@ Charles Chan <charleswhchan@users.noreply.github.com>
Charles Law <claw@conduce.com> Charles Law <claw@conduce.com>
Charles Smith <charles.smith@docker.com> Charles Smith <charles.smith@docker.com>
Charlie Drage <charlie@charliedrage.com> Charlie Drage <charlie@charliedrage.com>
Charlotte Mach <charlotte.mach@fs.lmu.de>
ChaYoung You <yousbe@gmail.com> ChaYoung You <yousbe@gmail.com>
Chee Hau Lim <cheehau.lim@mobimeo.com>
Chen Chuanliang <chen.chuanliang@zte.com.cn> Chen Chuanliang <chen.chuanliang@zte.com.cn>
Chen Hanxiao <chenhanxiao@cn.fujitsu.com> Chen Hanxiao <chenhanxiao@cn.fujitsu.com>
Chen Mingjie <chenmingjie0828@163.com> Chen Mingjie <chenmingjie0828@163.com>
Chen Qiu <cheney-90@hotmail.com> Chen Qiu <cheney-90@hotmail.com>
Ching Wei Kang <164879897+WilliamK112@users.noreply.github.com>
Chris Chinchilla <chris@chrischinchilla.com>
Chris Couzens <ccouzens@gmail.com>
Chris Gavin <chris@chrisgavin.me> Chris Gavin <chris@chrisgavin.me>
Chris Gibson <chris@chrisg.io> Chris Gibson <chris@chrisg.io>
Chris McKinnel <chrismckinnel@gmail.com> Chris McKinnel <chrismckinnel@gmail.com>
Chris Snow <chsnow123@gmail.com> Chris Snow <chsnow123@gmail.com>
Chris Vermilion <christopher.vermilion@gmail.com>
Chris Weyl <cweyl@alumni.drew.edu> Chris Weyl <cweyl@alumni.drew.edu>
Christian Persson <saser@live.se> Christian Persson <saser@live.se>
Christian Stefanescu <st.chris@gmail.com> Christian Stefanescu <st.chris@gmail.com>
@@ -178,48 +120,32 @@ Christophe Vidal <kriss@krizalys.com>
Christopher Biscardi <biscarch@sketcht.com> Christopher Biscardi <biscarch@sketcht.com>
Christopher Crone <christopher.crone@docker.com> Christopher Crone <christopher.crone@docker.com>
Christopher Jones <tophj@linux.vnet.ibm.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> Christy Norman <christy@linux.vnet.ibm.com>
Chun Chen <ramichen@tencent.com> Chun Chen <ramichen@tencent.com>
Clinton Kitson <clintonskitson@gmail.com> Clinton Kitson <clintonskitson@gmail.com>
Codex <codex@openai.com>
Coenraad Loubser <coenraad@wish.org.za> Coenraad Loubser <coenraad@wish.org.za>
Colin Hebert <hebert.colin@gmail.com> Colin Hebert <hebert.colin@gmail.com>
Collin Guarino <collin.guarino@gmail.com> Collin Guarino <collin.guarino@gmail.com>
Colm Hally <colmhally@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 Farrell <git@cfware.com>
Corey Quon <corey.quon@docker.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> Craig Wilhite <crwilhit@microsoft.com>
Cristian Staretu <cristian.staretu@gmail.com> Cristian Staretu <cristian.staretu@gmail.com>
Daehyeok Mun <daehyeok@gmail.com> Daehyeok Mun <daehyeok@gmail.com>
Dafydd Crosby <dtcrsby@gmail.com> Dafydd Crosby <dtcrsby@gmail.com>
Daisuke Ito <itodaisuke00@gmail.com>
dalanlan <dalanlan925@gmail.com> dalanlan <dalanlan925@gmail.com>
Damien Nadé <github@livna.org> Damien Nadé <github@livna.org>
Dan Cotora <dan@bluevision.ro> Dan Cotora <dan@bluevision.ro>
Dan Wallis <dan@wallis.nz>
Danial Gharib <danial.mail.gh@gmail.com>
Daniel Artine <daniel.artine@ufrj.br>
Daniel Cassidy <mail@danielcassidy.me.uk>
Daniel Dao <dqminh@cloudflare.com> Daniel Dao <dqminh@cloudflare.com>
Daniel Farrell <dfarrell@redhat.com> Daniel Farrell <dfarrell@redhat.com>
Daniel Gasienica <daniel@gasienica.ch> Daniel Gasienica <daniel@gasienica.ch>
Daniel Goosen <daniel.goosen@surveysampling.com> Daniel Goosen <daniel.goosen@surveysampling.com>
Daniel Helfand <dhelfand@redhat.com>
Daniel Hiltgen <daniel.hiltgen@docker.com> Daniel Hiltgen <daniel.hiltgen@docker.com>
Daniel J Walsh <dwalsh@redhat.com> Daniel J Walsh <dwalsh@redhat.com>
Daniel Nephin <dnephin@docker.com> Daniel Nephin <dnephin@docker.com>
Daniel Norberg <dano@spotify.com> Daniel Norberg <dano@spotify.com>
Daniel Watkins <daniel@daniel-watkins.co.uk> Daniel Watkins <daniel@daniel-watkins.co.uk>
Daniel Zhang <jmzwcn@gmail.com> Daniel Zhang <jmzwcn@gmail.com>
Daniil Nikolenko <qoo2p5@gmail.com>
Danny Berger <dpb587@gmail.com> Danny Berger <dpb587@gmail.com>
Darren Shepherd <darren.s.shepherd@gmail.com> Darren Shepherd <darren.s.shepherd@gmail.com>
Darren Stahl <darst@microsoft.com> Darren Stahl <darst@microsoft.com>
@@ -227,69 +153,51 @@ Dattatraya Kumbhar <dattatraya.kumbhar@gslab.com>
Dave Goodchild <buddhamagnet@gmail.com> Dave Goodchild <buddhamagnet@gmail.com>
Dave Henderson <dhenderson@gmail.com> Dave Henderson <dhenderson@gmail.com>
Dave Tucker <dt@docker.com> Dave Tucker <dt@docker.com>
David Alvarez <david.alvarez@flyeralarm.com>
David Beitey <david@davidjb.com> David Beitey <david@davidjb.com>
David Calavera <david.calavera@gmail.com> David Calavera <david.calavera@gmail.com>
David Cramer <davcrame@cisco.com> David Cramer <davcrame@cisco.com>
David Dooling <david.dooling@docker.com> David Dooling <dooling@gmail.com>
David Gageot <david@gageot.net> 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 Lechner <david@lechnology.com>
David Scott <dave@recoil.org> David Scott <dave@recoil.org>
David Sheets <dsheets@docker.com> David Sheets <dsheets@docker.com>
David Williamson <david.williamson@docker.com> David Williamson <david.williamson@docker.com>
David Xia <dxia@spotify.com> David Xia <dxia@spotify.com>
David Young <yangboh@cn.ibm.com> David Young <yangboh@cn.ibm.com>
Davlat Davydov <literally_user@hotmail.com>
Deng Guangxing <dengguangxing@huawei.com> Deng Guangxing <dengguangxing@huawei.com>
Denis Defreyne <denis@soundcloud.com> Denis Defreyne <denis@soundcloud.com>
Denis Gladkikh <denis@gladkikh.email> Denis Gladkikh <denis@gladkikh.email>
Denis Ollier <larchunix@users.noreply.github.com> Denis Ollier <larchunix@users.noreply.github.com>
Dennis Docter <dennis@d23.nl> Dennis Docter <dennis@d23.nl>
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Derek McGowan <derek@mcgstyle.net>
Derek McGowan <derek@mcg.dev>
Derek Misler <derek.misler@docker.com>
Des Preston <despreston@gmail.com>
Deshi Xiao <dxiao@redhat.com> Deshi Xiao <dxiao@redhat.com>
Dharmit Shah <shahdharmit@gmail.com> Dharmit Shah <shahdharmit@gmail.com>
Dhawal Yogesh Bhanushali <dbhanushali@vmware.com> Dhawal Yogesh Bhanushali <dbhanushali@vmware.com>
Dieter Reuter <dieter.reuter@me.com> Dieter Reuter <dieter.reuter@me.com>
Dilep Dev <34891655+DilepDev@users.noreply.github.com>
Dima Stopel <dima@twistlock.com> Dima Stopel <dima@twistlock.com>
Dimitry Andric <d.andric@activevideo.com> Dimitry Andric <d.andric@activevideo.com>
Ding Fei <dingfei@stars.org.cn> Ding Fei <dingfei@stars.org.cn>
Diogo Monica <diogo@docker.com> Diogo Monica <diogo@docker.com>
Djordje Lukic <djordje.lukic@docker.com>
Dmitriy Fishman <fishman.code@gmail.com>
Dmitry Gusev <dmitry.gusev@gmail.com> Dmitry Gusev <dmitry.gusev@gmail.com>
Dmitry Smirnov <onlyjob@member.fsf.org> Dmitry Smirnov <onlyjob@member.fsf.org>
Dmitry V. Krivenok <krivenok.dmitry@gmail.com> Dmitry V. Krivenok <krivenok.dmitry@gmail.com>
Dominik Braun <dominik.braun@nbsp.de>
Don Kjer <don.kjer@gmail.com> Don Kjer <don.kjer@gmail.com>
Dong Chen <dongluo.chen@docker.com> Dong Chen <dongluo.chen@docker.com>
DongGeon Lee <secmatth1996@gmail.com>
Dorin Geman <dorin.geman@docker.com>
Doug Davis <dug@us.ibm.com> Doug Davis <dug@us.ibm.com>
Drew Erny <derny@mirantis.com> Drew Erny <drew.erny@docker.com>
Ed Costello <epc@epcostello.com> Ed Costello <epc@epcostello.com>
Ed Morley <501702+edmorley@users.noreply.github.com>
Elango Sivanandam <elango.siva@docker.com> Elango Sivanandam <elango.siva@docker.com>
Eli Uriegas <eli.uriegas@docker.com> Eli Uriegas <eli.uriegas@docker.com>
Eli Uriegas <seemethere101@gmail.com> Eli Uriegas <seemethere101@gmail.com>
Elias Faxö <elias.faxo@tre.se> Elias Faxö <elias.faxo@tre.se>
Elliot Luo <956941328@qq.com> Elliot Luo <956941328@qq.com>
Eng Zer Jun <engzerjun@gmail.com>
Eric Bode <eric.bode@foundries.io>
Eric Curtin <ericcurtin17@gmail.com> Eric Curtin <ericcurtin17@gmail.com>
Eric Engestrom <eric@engestrom.ch>
Eric G. Noriega <enoriega@vizuri.com> Eric G. Noriega <enoriega@vizuri.com>
Eric Rosenberg <ehaydenr@gmail.com> Eric Rosenberg <ehaydenr@gmail.com>
Eric Sage <eric.david.sage@gmail.com> Eric Sage <eric.david.sage@gmail.com>
Eric-Olivier Lamey <eo@lamey.me> Eric-Olivier Lamey <eo@lamey.me>
Erica Windisch <erica@windisch.us> Erica Windisch <erica@windisch.us>
Erik Hollensbe <github@hollensbe.org> Erik Hollensbe <github@hollensbe.org>
Erik Humphrey <erik.humphrey@carleton.ca>
Erik St. Martin <alakriti@gmail.com> Erik St. Martin <alakriti@gmail.com>
Essam A. Hassan <es.hassan187@gmail.com> Essam A. Hassan <es.hassan187@gmail.com>
Ethan Haynes <ethanhaynes@alumni.harvard.edu> Ethan Haynes <ethanhaynes@alumni.harvard.edu>
@@ -298,19 +206,15 @@ Eugene Yakubovich <eugene.yakubovich@coreos.com>
Evan Allrich <evan@unguku.com> Evan Allrich <evan@unguku.com>
Evan Hazlett <ejhazlett@gmail.com> Evan Hazlett <ejhazlett@gmail.com>
Evan Krall <krall@yelp.com> Evan Krall <krall@yelp.com>
Evan Lezar <elezar@nvidia.com>
Evelyn Xu <evelynhsu21@gmail.com> Evelyn Xu <evelynhsu21@gmail.com>
Everett Toews <everett.toews@rackspace.com> Everett Toews <everett.toews@rackspace.com>
Fabio Falci <fabiofalci@gmail.com> Fabio Falci <fabiofalci@gmail.com>
Fabrizio Soppelsa <fsoppelsa@mirantis.com> Fabrizio Soppelsa <fsoppelsa@mirantis.com>
Felix Geyer <debfx@fobos.de>
Felix Hupfeld <felix@quobyte.com> Felix Hupfeld <felix@quobyte.com>
Felix Rabe <felix@rabe.io> Felix Rabe <felix@rabe.io>
fezzik1620 <fezzik1620@users.noreply.github.com>
Filip Jareš <filipjares@gmail.com> Filip Jareš <filipjares@gmail.com>
Flavio Crisciani <flavio.crisciani@docker.com> Flavio Crisciani <flavio.crisciani@docker.com>
Florian Klein <florian.klein@free.fr> Florian Klein <florian.klein@free.fr>
Forest Johnson <fjohnson@peoplenetonline.com>
Foysal Iqbal <foysal.iqbal.fb@gmail.com> Foysal Iqbal <foysal.iqbal.fb@gmail.com>
François Scala <francois.scala@swiss-as.com> François Scala <francois.scala@swiss-as.com>
Fred Lifton <fred.lifton@docker.com> Fred Lifton <fred.lifton@docker.com>
@@ -318,34 +222,21 @@ Frederic Hemberger <mail@frederic-hemberger.de>
Frederick F. Kautz IV <fkautz@redhat.com> Frederick F. Kautz IV <fkautz@redhat.com>
Frederik Nordahl Jul Sabroe <frederikns@gmail.com> Frederik Nordahl Jul Sabroe <frederikns@gmail.com>
Frieder Bluemle <frieder.bluemle@gmail.com> Frieder Bluemle <frieder.bluemle@gmail.com>
Gabriel Gore <gabgore@cisco.com>
Gabriel Nicolas Avellaneda <avellaneda.gabriel@gmail.com> Gabriel Nicolas Avellaneda <avellaneda.gabriel@gmail.com>
Gabriela Georgieva <gabriela.georgieva@docker.com>
Gaetan de Villele <gdevillele@gmail.com> Gaetan de Villele <gdevillele@gmail.com>
Gang Qiao <qiaohai8866@gmail.com> Gang Qiao <qiaohai8866@gmail.com>
Gary Schaetz <gary@schaetzkc.com> Gary Schaetz <gary@schaetzkc.com>
Genki Takiuchi <genki@s21g.com> Genki Takiuchi <genki@s21g.com>
George MacRorie <gmacr31@gmail.com> George MacRorie <gmacr31@gmail.com>
George Margaritis <gmargaritis@protonmail.com>
George Xie <georgexsh@gmail.com> George Xie <georgexsh@gmail.com>
Gianluca Borello <g.borello@gmail.com> Gianluca Borello <g.borello@gmail.com>
Giau. Tran Minh <hello@giautm.dev>
Giedrius Jonikas <giedriusj1@gmail.com>
Gildas Cuisinier <gildas.cuisinier@gcuisinier.net> Gildas Cuisinier <gildas.cuisinier@gcuisinier.net>
Gio d'Amelio <giodamelio@gmail.com>
Gleb Stsenov <gleb.stsenov@gmail.com>
Goksu Toprak <goksu.toprak@docker.com>
Gou Rao <gou@portworx.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> Grant Reaber <grant.reaber@gmail.com>
Greg Pflaum <gpflaum@users.noreply.github.com> Greg Pflaum <gpflaum@users.noreply.github.com>
Gsealy <jiaojingwei1001@hotmail.com>
Guilhem Lettron <guilhem+github@lettron.fr> Guilhem Lettron <guilhem+github@lettron.fr>
Guillaume J. Charmes <guillaume.charmes@docker.com> Guillaume J. Charmes <guillaume.charmes@docker.com>
Guillaume Le Floch <glfloch@gmail.com> Guillaume Le Floch <glfloch@gmail.com>
Guillaume Tardif <guillaume.tardif@gmail.com>
gwx296173 <gaojing3@huawei.com> gwx296173 <gaojing3@huawei.com>
Günther Jungbluth <gunther@gameslabs.net> Günther Jungbluth <gunther@gameslabs.net>
Hakan Özler <hakan.ozler@kodcu.com> Hakan Özler <hakan.ozler@kodcu.com>
@@ -354,22 +245,16 @@ Harald Albers <github@albersweb.de>
Harold Cooper <hrldcpr@gmail.com> Harold Cooper <hrldcpr@gmail.com>
Harry Zhang <harryz@hyper.sh> Harry Zhang <harryz@hyper.sh>
He Simei <hesimei@zju.edu.cn> He Simei <hesimei@zju.edu.cn>
Hector S <hfsam88@gmail.com>
Helen Xie <chenjg@harmonycloud.cn> Helen Xie <chenjg@harmonycloud.cn>
Henning Sprang <henning.sprang@gmail.com> Henning Sprang <henning.sprang@gmail.com>
Henry N <henrynmail-github@yahoo.de> Henry N <henrynmail-github@yahoo.de>
Hernan Garcia <hernandanielg@gmail.com> Hernan Garcia <hernandanielg@gmail.com>
Hongbin Lu <hongbin034@gmail.com> Hongbin Lu <hongbin034@gmail.com>
Hossein Abbasi <16090309+hsnabszhdn@users.noreply.github.com>
Hu Keping <hukeping@huawei.com> Hu Keping <hukeping@huawei.com>
Huayi Zhang <irachex@gmail.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> huqun <huqun@zju.edu.cn>
Huu Nguyen <huu@prismskylabs.com> Huu Nguyen <huu@prismskylabs.com>
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com> Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com>
Iain MacDonald <IJMacD@gmail.com>
Iain Samuel McLean Elder <iain@isme.es>
Ian Campbell <ian.campbell@docker.com> Ian Campbell <ian.campbell@docker.com>
Ian Philpot <ian.philpot@microsoft.com> Ian Philpot <ian.philpot@microsoft.com>
Ignacio Capurro <icapurrofagian@gmail.com> Ignacio Capurro <icapurrofagian@gmail.com>
@@ -379,16 +264,12 @@ Ilya Sotkov <ilya@sotkov.com>
Ioan Eugen Stan <eu@ieugen.ro> Ioan Eugen Stan <eu@ieugen.ro>
Isabel Jimenez <contact.isabeljimenez@gmail.com> Isabel Jimenez <contact.isabeljimenez@gmail.com>
Ivan Grcic <igrcic@gmail.com> Ivan Grcic <igrcic@gmail.com>
Ivan Grund <ivan.grund@gmail.com>
Ivan Markin <sw@nogoegst.net> Ivan Markin <sw@nogoegst.net>
Jacob Atzen <jacob@jacobatzen.dk> Jacob Atzen <jacob@jacobatzen.dk>
Jacob Tomlinson <jacob@tom.linson.uk> Jacob Tomlinson <jacob@tom.linson.uk>
Jacopo Rigoli <rigoli.jacopo@gmail.com>
Jaivish Kothari <janonymous.codevulture@gmail.com> Jaivish Kothari <janonymous.codevulture@gmail.com>
Jake Lambert <jake.lambert@volusion.com> Jake Lambert <jake.lambert@volusion.com>
Jake Sanders <jsand@google.com> Jake Sanders <jsand@google.com>
Jake Stokes <contactjake@developerjake.com>
Jakub Panek <me@panekj.dev>
James Nesbitt <james.nesbitt@wunderkraut.com> James Nesbitt <james.nesbitt@wunderkraut.com>
James Turnbull <james@lovedthanlost.net> James Turnbull <james@lovedthanlost.net>
Jamie Hannaford <jamie@limetree.org> Jamie Hannaford <jamie@limetree.org>
@@ -398,38 +279,31 @@ Jan-Jaap Driessen <janjaapdriessen@gmail.com>
Jana Radhakrishnan <mrjana@docker.com> Jana Radhakrishnan <mrjana@docker.com>
Jared Hocutt <jaredh@netapp.com> Jared Hocutt <jaredh@netapp.com>
Jasmine Hegman <jasmine@jhegman.com> Jasmine Hegman <jasmine@jhegman.com>
Jason Hall <jason@chainguard.dev>
Jason Heiss <jheiss@aput.net> Jason Heiss <jheiss@aput.net>
Jason Plum <jplum@devonit.com> Jason Plum <jplum@devonit.com>
Jay Kamat <github@jgkamat.33mail.com> Jay Kamat <github@jgkamat.33mail.com>
Jean Lecordier <jeanlecordier@hotmail.fr>
Jean Rouge <rougej+github@gmail.com> Jean Rouge <rougej+github@gmail.com>
Jean-Christophe Sirot <jean-christophe.sirot@docker.com> Jean-Christophe Sirot <jean-christophe.sirot@docker.com>
Jean-Pierre Huynh <jean-pierre.huynh@ounet.fr> Jean-Pierre Huynh <jean-pierre.huynh@ounet.fr>
Jeff Lindsay <progrium@gmail.com> Jeff Lindsay <progrium@gmail.com>
Jeff Nickoloff <jeff.nickoloff@gmail.com> Jeff Nickoloff <jeff.nickoloff@gmail.com>
Jeff Silberman <jsilberm@gmail.com> Jeff Silberman <jsilberm@gmail.com>
Jennings Zhang <jenni_zh@protonmail.com>
Jeremy Chambers <jeremy@thehipbot.com> Jeremy Chambers <jeremy@thehipbot.com>
Jeremy Unruh <jeremybunruh@gmail.com> Jeremy Unruh <jeremybunruh@gmail.com>
Jeremy Yallop <yallop@docker.com> Jeremy Yallop <yallop@docker.com>
Jeroen Franse <jeroenfranse@gmail.com> Jeroen Franse <jeroenfranse@gmail.com>
Jesse Adametz <jesseadametz@gmail.com> Jesse Adametz <jesseadametz@gmail.com>
Jessica Frazelle <jess@oxide.computer> Jessica Frazelle <jessfraz@google.com>
Jezeniel Zapanta <jpzapanta22@gmail.com> Jezeniel Zapanta <jpzapanta22@gmail.com>
Jian Zhang <zhangjian.fnst@cn.fujitsu.com> Jian Zhang <zhangjian.fnst@cn.fujitsu.com>
Jianyong Wu <wujianyong@hygon.cn>
Jie Luo <luo612@zju.edu.cn> Jie Luo <luo612@zju.edu.cn>
Jilles Oldenbeuving <ojilles@gmail.com> Jilles Oldenbeuving <ojilles@gmail.com>
Jim Chen <njucjc@gmail.com>
Jim Galasyn <jim.galasyn@docker.com> Jim Galasyn <jim.galasyn@docker.com>
Jim Lin <b04705003@ntu.edu.tw>
Jimmy Leger <jimmy.leger@gmail.com> Jimmy Leger <jimmy.leger@gmail.com>
Jimmy Song <rootsongjc@gmail.com> Jimmy Song <rootsongjc@gmail.com>
jimmyxian <jimmyxian2004@yahoo.com.cn> jimmyxian <jimmyxian2004@yahoo.com.cn>
Jintao Zhang <zhangjintao9020@gmail.com> Jintao Zhang <zhangjintao9020@gmail.com>
Joao Fernandes <joao.fernandes@docker.com> Joao Fernandes <joao.fernandes@docker.com>
Joe Abbey <joe.abbey@gmail.com>
Joe Doliner <jdoliner@pachyderm.io> Joe Doliner <jdoliner@pachyderm.io>
Joe Gordon <joe.gordon0@gmail.com> Joe Gordon <joe.gordon0@gmail.com>
Joel Handwell <joelhandwell@gmail.com> Joel Handwell <joelhandwell@gmail.com>
@@ -439,8 +313,7 @@ Johan Euphrosine <proppy@google.com>
Johannes 'fish' Ziemke <github@freigeist.org> Johannes 'fish' Ziemke <github@freigeist.org>
John Feminella <jxf@jxf.me> John Feminella <jxf@jxf.me>
John Harris <john@johnharris.io> John Harris <john@johnharris.io>
John Howard <github@lowenna.com> John Howard (VM) <John.Howard@microsoft.com>
John Howard <howardjohn@google.com>
John Laswell <john.n.laswell@gmail.com> John Laswell <john.n.laswell@gmail.com>
John Maguire <jmaguire@duosecurity.com> John Maguire <jmaguire@duosecurity.com>
John Mulhausen <john@docker.com> John Mulhausen <john@docker.com>
@@ -449,39 +322,28 @@ John Stephens <johnstep@docker.com>
John Tims <john.k.tims@gmail.com> John Tims <john.k.tims@gmail.com>
John V. Martinez <jvmatl@gmail.com> John V. Martinez <jvmatl@gmail.com>
John Willis <john.willis@docker.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 Boulle <jonathanboulle@gmail.com>
Jonathan Lee <jonjohn1232009@gmail.com> Jonathan Lee <jonjohn1232009@gmail.com>
Jonathan Lomas <jonathan@floatinglomas.ca> Jonathan Lomas <jonathan@floatinglomas.ca>
Jonathan McCrohan <jmccrohan@gmail.com> Jonathan McCrohan <jmccrohan@gmail.com>
Jonathan Warriss-Simmons <misterws@diogenes.ws>
Jonh Wendell <jonh.wendell@redhat.com> Jonh Wendell <jonh.wendell@redhat.com>
Jordan Jennings <jjn2009@gmail.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> Joseph Kern <jkern@semafour.net>
Josh Bodah <jb3689@yahoo.com> Josh Bodah <jb3689@yahoo.com>
Josh Chorlton <jchorlton@gmail.com> Josh Chorlton <jchorlton@gmail.com>
Josh Hawn <josh.hawn@docker.com> Josh Hawn <josh.hawn@docker.com>
Josh Horwitz <horwitz@addthis.com> Josh Horwitz <horwitz@addthis.com>
Josh Soref <jsoref@gmail.com> Josh Soref <jsoref@gmail.com>
Julian <gitea+julian@ic.thejulian.uk>
Julien Barbier <write0@gmail.com> Julien Barbier <write0@gmail.com>
Julien Kassar <github@kassisol.com> Julien Kassar <github@kassisol.com>
Julien Maitrehenry <julien.maitrehenry@me.com> Julien Maitrehenry <julien.maitrehenry@me.com>
Julio Cesar Garcia <juliogarciamelgarejo@gmail.com>
Justas Brazauskas <brazauskasjustas@gmail.com> Justas Brazauskas <brazauskasjustas@gmail.com>
Justin Chadwell <me@jedevc.com>
Justin Cormack <justin.cormack@docker.com> Justin Cormack <justin.cormack@docker.com>
Justin Simonelis <justin.p.simonelis@gmail.com> Justin Simonelis <justin.p.simonelis@gmail.com>
Justyn Temme <justyntemme@gmail.com> Justyn Temme <justyntemme@gmail.com>
Jyrki Puttonen <jyrkiput@gmail.com> Jyrki Puttonen <jyrkiput@gmail.com>
Jérémie Drouet <jeremie.drouet@gmail.com> Jérémie Drouet <jeremie.drouet@gmail.com>
Jérôme Petazzoni <jerome.petazzoni@docker.com> Jérôme Petazzoni <jerome.petazzoni@docker.com>
Jörg Sommer <joerg@jo-so.de>
Jörg Thalheim <joerg@higgsboson.tk> Jörg Thalheim <joerg@higgsboson.tk>
Kai Blin <kai@samba.org> Kai Blin <kai@samba.org>
Kai Qiang Wu (Kennan) <wkq5325@gmail.com> Kai Qiang Wu (Kennan) <wkq5325@gmail.com>
@@ -489,52 +351,42 @@ Kara Alexandra <kalexandra@us.ibm.com>
Kareem Khazem <karkhaz@karkhaz.com> Kareem Khazem <karkhaz@karkhaz.com>
Karthik Nayak <Karthik.188@gmail.com> Karthik Nayak <Karthik.188@gmail.com>
Kat Samperi <kat.samperi@gmail.com> Kat Samperi <kat.samperi@gmail.com>
Kathryn Spiers <kathryn@spiers.me>
Katie McLaughlin <katie@glasnt.com> Katie McLaughlin <katie@glasnt.com>
Ke Xu <leonhartx.k@gmail.com> Ke Xu <leonhartx.k@gmail.com>
Kei Ohmura <ohmura.kei@gmail.com> Kei Ohmura <ohmura.kei@gmail.com>
Keith Hudgins <greenman@greenman.org> Keith Hudgins <greenman@greenman.org>
Kelton Bassingthwaite <KeltonBassingthwaite@gmail.com>
Ken Cochrane <kencochrane@gmail.com> Ken Cochrane <kencochrane@gmail.com>
Ken ICHIKAWA <ichikawa.ken@jp.fujitsu.com> Ken ICHIKAWA <ichikawa.ken@jp.fujitsu.com>
Kenfe-Mickaël Laventure <mickael.laventure@gmail.com> Kenfe-Mickaël Laventure <mickael.laventure@gmail.com>
Kevin Alvarez <github@crazymax.dev>
Kevin Burke <kev@inburke.com> Kevin Burke <kev@inburke.com>
Kevin Feyrer <kevin.feyrer@btinternet.com> Kevin Feyrer <kevin.feyrer@btinternet.com>
Kevin Kern <kaiwentan@harmonycloud.cn> Kevin Kern <kaiwentan@harmonycloud.cn>
Kevin Kirsche <Kev.Kirsche+GitHub@gmail.com> Kevin Kirsche <Kev.Kirsche+GitHub@gmail.com>
Kevin Meredith <kevin.m.meredith@gmail.com> Kevin Meredith <kevin.m.meredith@gmail.com>
Kevin Richardson <kevin@kevinrichardson.co> Kevin Richardson <kevin@kevinrichardson.co>
Kevin Woblick <mail@kovah.de>
khaled souf <khaled.souf@gmail.com> khaled souf <khaled.souf@gmail.com>
Kim Eik <kim@heldig.org> Kim Eik <kim@heldig.org>
Kir Kolyshkin <kolyshkin@gmail.com> Kir Kolyshkin <kolyshkin@gmail.com>
Kirill A. Korinsky <kirill@korins.ky>
Kotaro Yoshimatsu <kotaro.yoshimatsu@gmail.com> Kotaro Yoshimatsu <kotaro.yoshimatsu@gmail.com>
Krasi Georgiev <krasi@vip-consult.solutions> Krasi Georgiev <krasi@vip-consult.solutions>
Kris-Mikael Krister <krismikael@protonmail.com> Kris-Mikael Krister <krismikael@protonmail.com>
Kun Zhang <zkazure@gmail.com> Kun Zhang <zkazure@gmail.com>
Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp> Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp>
Kyle Mitofsky <Kylemit@gmail.com> Kyle Spiers <kyle@spiers.me>
Lachlan Cooper <lachlancooper@gmail.com> Lachlan Cooper <lachlancooper@gmail.com>
Lai Jiangshan <jiangshanlai@gmail.com> Lai Jiangshan <jiangshanlai@gmail.com>
Lajos Papp <lajos.papp@sequenceiq.com>
Lars Kellogg-Stedman <lars@redhat.com> Lars Kellogg-Stedman <lars@redhat.com>
Laura Brehm <laurabrehm@hey.com>
Laura Frank <ljfrank@gmail.com> Laura Frank <ljfrank@gmail.com>
Laurent Erignoux <lerignoux@gmail.com> Laurent Erignoux <lerignoux@gmail.com>
Laurent Goderre <laurent.goderre@docker.com>
Lee Gaines <eightlimbed@gmail.com> Lee Gaines <eightlimbed@gmail.com>
Lei Jitang <leijitang@huawei.com> Lei Jitang <leijitang@huawei.com>
Lennie <github@consolejunkie.net> Lennie <github@consolejunkie.net>
lentil32 <lentil32@icloud.com>
Leo Gallucci <elgalu3@gmail.com> Leo Gallucci <elgalu3@gmail.com>
Leonid Skorospelov <leosko94@gmail.com>
Lewis Daly <lewisdaly@me.com> Lewis Daly <lewisdaly@me.com>
Li Fu Bang <lifubang@acmcoder.com>
Li Yi <denverdino@gmail.com> Li Yi <denverdino@gmail.com>
Li Zeghong <zeghong@hotmail.com> Li Yi <weiyuan.yl@alibaba-inc.com>
Liang-Chi Hsieh <viirya@gmail.com> Liang-Chi Hsieh <viirya@gmail.com>
Lifubang <lifubang@acmcoder.com>
Lihua Tang <lhtang@alauda.io> Lihua Tang <lhtang@alauda.io>
Lily Guo <lily.guo@docker.com> Lily Guo <lily.guo@docker.com>
Lin Lu <doraalin@163.com> Lin Lu <doraalin@163.com>
@@ -546,39 +398,26 @@ lixiaobing10051267 <li.xiaobing1@zte.com.cn>
Lloyd Dewolf <foolswisdom@gmail.com> Lloyd Dewolf <foolswisdom@gmail.com>
Lorenzo Fontana <lo@linux.com> Lorenzo Fontana <lo@linux.com>
Louis Opter <kalessin@kalessin.fr> Louis Opter <kalessin@kalessin.fr>
Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
Luca Favatella <luca.favatella@erlang-solutions.com> Luca Favatella <luca.favatella@erlang-solutions.com>
Luca Marturana <lucamarturana@gmail.com> Luca Marturana <lucamarturana@gmail.com>
Lucas Chan <lucas-github@lucaschan.com> Lucas Chan <lucas-github@lucaschan.com>
Ludovic Temgoua Abanda <abandaludovic500@gmail.com>
Luis Henrique Mulinari <luis.mulinari@gmail.com>
Luka Hartwig <mail@lukahartwig.de> Luka Hartwig <mail@lukahartwig.de>
Lukas Heeren <lukas-heeren@hotmail.com>
Lukas Michael <lukas.23022005@gmail.com>
Lukasz Zajaczkowski <Lukasz.Zajaczkowski@ts.fujitsu.com> Lukasz Zajaczkowski <Lukasz.Zajaczkowski@ts.fujitsu.com>
Luo Jiyin <luojiyin@hotmail.com>
Lydell Manganti <LydellManganti@users.noreply.github.com> Lydell Manganti <LydellManganti@users.noreply.github.com>
Lénaïc Huard <lhuard@amadeus.com> Lénaïc Huard <lhuard@amadeus.com>
Ma Shimiao <mashimiao.fnst@cn.fujitsu.com> Ma Shimiao <mashimiao.fnst@cn.fujitsu.com>
Mabin <bin.ma@huawei.com> Mabin <bin.ma@huawei.com>
Maciej Kalisz <maciej.d.kalisz@gmail.com>
Madhav Puri <madhav.puri@gmail.com> Madhav Puri <madhav.puri@gmail.com>
Madhu Venugopal <madhu@socketplane.io> Madhu Venugopal <madhu@socketplane.io>
Madhur Batra <madhurbatra097@gmail.com>
Mahesh Thakur <maheshthakur9152@gmail.com>
Malte Janduda <mail@janduda.net> Malte Janduda <mail@janduda.net>
Manjunath A Kumatagi <mkumatag@in.ibm.com> Manjunath A Kumatagi <mkumatag@in.ibm.com>
Mansi Nahar <mmn4185@rit.edu> Mansi Nahar <mmn4185@rit.edu>
mapk0y <mapk0y@gmail.com> mapk0y <mapk0y@gmail.com>
Marc Bihlmaier <marc.bihlmaier@reddoxx.com> Marc Bihlmaier <marc.bihlmaier@reddoxx.com>
Marc Cornellà <hello@mcornella.com>
Marco Mariani <marco.mariani@alterway.fr> Marco Mariani <marco.mariani@alterway.fr>
Marco Spiess <marco.spiess@hotmail.de>
Marco Vedovati <mvedovati@suse.com> Marco Vedovati <mvedovati@suse.com>
Marcus Martins <marcus@docker.com> Marcus Martins <marcus@docker.com>
Marianna Tessel <mtesselh@gmail.com> Marianna Tessel <mtesselh@gmail.com>
Marius Ileana <marius.ileana@gmail.com>
Marius Meschter <marius@meschter.me>
Marius Sturm <marius@graylog.com> Marius Sturm <marius@graylog.com>
Mark Oates <fl0yd@me.com> Mark Oates <fl0yd@me.com>
Marsh Macy <marsma@microsoft.com> Marsh Macy <marsma@microsoft.com>
@@ -587,30 +426,20 @@ Mary Anthony <mary.anthony@docker.com>
Mason Fish <mason.fish@docker.com> Mason Fish <mason.fish@docker.com>
Mason Malone <mason.malone@gmail.com> Mason Malone <mason.malone@gmail.com>
Mateusz Major <apkd@users.noreply.github.com> Mateusz Major <apkd@users.noreply.github.com>
Mathias Duedahl <64321057+Lussebullen@users.noreply.github.com>
Mathieu Champlon <mathieu.champlon@docker.com> Mathieu Champlon <mathieu.champlon@docker.com>
Mathieu Rollet <matletix@gmail.com>
Matt Gucci <matt9ucci@gmail.com> Matt Gucci <matt9ucci@gmail.com>
Matt Robenolt <matt@ydekproductions.com> Matt Robenolt <matt@ydekproductions.com>
Matteo Orefice <matteo.orefice@bites4bits.software> Matteo Orefice <matteo.orefice@bites4bits.software>
Matteo Panzeri <matteo1782@gmail.com>
Matthew Heon <mheon@redhat.com> Matthew Heon <mheon@redhat.com>
Matthieu Hauglustaine <matt.hauglustaine@gmail.com> Matthieu Hauglustaine <matt.hauglustaine@gmail.com>
Matthieu MOREL <matthieu.morel35@gmail.com>
Mauro Porras P <mauroporrasp@gmail.com> Mauro Porras P <mauroporrasp@gmail.com>
Max Morozov <gtmax.yo@gmail.com>
Max Shytikov <mshytikov@gmail.com> Max Shytikov <mshytikov@gmail.com>
Max-Julian Pogner <max-julian@pogner.at>
Maxime Petazzoni <max@signalfuse.com> Maxime Petazzoni <max@signalfuse.com>
Maximillian Fan Xavier <maximillianfx@gmail.com>
Mei ChunTao <mei.chuntao@zte.com.cn> Mei ChunTao <mei.chuntao@zte.com.cn>
Melroy van den Berg <melroy@melroy.org>
Mert Şişmanoğlu <mert190737fb@gmail.com>
Metal <2466052+tedhexaflow@users.noreply.github.com>
Micah Zoltu <micah@newrelic.com> Micah Zoltu <micah@newrelic.com>
Michael A. Smith <michael@smith-li.com> Michael A. Smith <michael@smith-li.com>
Michael Bridgen <mikeb@squaremobius.net> Michael Bridgen <mikeb@squaremobius.net>
Michael Crosby <crosbymichael@gmail.com> Michael Crosby <michael@docker.com>
Michael Friis <friism@gmail.com> Michael Friis <friism@gmail.com>
Michael Irwin <mikesir87@gmail.com> Michael Irwin <mikesir87@gmail.com>
Michael Käufl <docker@c.michael-kaeufl.de> Michael Käufl <docker@c.michael-kaeufl.de>
@@ -618,44 +447,31 @@ Michael Prokop <github@michael-prokop.at>
Michael Scharf <github@scharf.gr> Michael Scharf <github@scharf.gr>
Michael Spetsiotis <michael_spets@hotmail.com> Michael Spetsiotis <michael_spets@hotmail.com>
Michael Steinert <mike.steinert@gmail.com> Michael Steinert <mike.steinert@gmail.com>
Michael Tews <michael@tews.dev>
Michael West <mwest@mdsol.com> Michael West <mwest@mdsol.com>
Michael Zampani <michael.zampani@docker.com>
Michal Minář <miminar@redhat.com> Michal Minář <miminar@redhat.com>
Michał Czeraszkiewicz <czerasz@gmail.com> Michał Czeraszkiewicz <czerasz@gmail.com>
Mickael Emirkanian <mickael.emirkanian@docker.com>
Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com> Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com>
Mihai Borobocea <MihaiBorob@gmail.com> Mihai Borobocea <MihaiBorob@gmail.com>
Mihuleacc Sergiu <mihuleac.sergiu@gmail.com> Mihuleacc Sergiu <mihuleac.sergiu@gmail.com>
Mike Brown <brownwm@us.ibm.com> Mike Brown <brownwm@us.ibm.com>
Mike Casas <mkcsas0@gmail.com> Mike Casas <mkcsas0@gmail.com>
Mike Dalton <mikedalton@github.com>
Mike Danese <mikedanese@google.com> Mike Danese <mikedanese@google.com>
Mike Dillon <mike@embody.org> Mike Dillon <mike@embody.org>
Mike Goelzer <mike.goelzer@docker.com> Mike Goelzer <mike.goelzer@docker.com>
Mike MacCana <mike.maccana@gmail.com> Mike MacCana <mike.maccana@gmail.com>
mikelinjie <294893458@qq.com> mikelinjie <294893458@qq.com>
Mikhail Vasin <vasin@cloud-tv.ru> Mikhail Vasin <vasin@cloud-tv.ru>
Milas Bowman <milas.bowman@docker.com>
Milind Chawre <milindchawre@gmail.com> Milind Chawre <milindchawre@gmail.com>
Mindaugas Rukas <momomg@gmail.com> Mindaugas Rukas <momomg@gmail.com>
Miroslav Gula <miroslav.gula@naytrolabs.com>
Misty Stanley-Jones <misty@docker.com> Misty Stanley-Jones <misty@docker.com>
Mohammad Banikazemi <mb@us.ibm.com> Mohammad Banikazemi <mb@us.ibm.com>
Mohammad Hossein <mhm98035@gmail.com>
Mohammed Aaqib Ansari <maaquib@gmail.com> Mohammed Aaqib Ansari <maaquib@gmail.com>
Mohammed Aminu Futa <mohammedfuta2000@gmail.com>
Mohammed Thaha <mohammedthahacse@gmail.com>
Mohini Anne Dsouza <mohini3917@gmail.com> Mohini Anne Dsouza <mohini3917@gmail.com>
Moorthy RS <rsmoorthy@gmail.com> Moorthy RS <rsmoorthy@gmail.com>
Morgan Bauer <mbauer@us.ibm.com> Morgan Bauer <mbauer@us.ibm.com>
Morten Hekkvang <morten.hekkvang@sbab.se>
Morten Linderud <morten@linderud.pw>
Moysés Borges <moysesb@gmail.com> Moysés Borges <moysesb@gmail.com>
Mozi <29089388+pzhlkj6612@users.noreply.github.com>
Mrunal Patel <mrunalp@gmail.com> Mrunal Patel <mrunalp@gmail.com>
muicoder <muicoder@gmail.com> muicoder <muicoder@gmail.com>
Murukesh Mohanan <murukesh.mohanan@gmail.com>
Muthukumar R <muthur@gmail.com> Muthukumar R <muthur@gmail.com>
Máximo Cuadros <mcuadros@gmail.com> Máximo Cuadros <mcuadros@gmail.com>
Mårten Cassel <marten.cassel@gmail.com> Mårten Cassel <marten.cassel@gmail.com>
@@ -671,8 +487,6 @@ Nathan LeClaire <nathan.leclaire@docker.com>
Nathan McCauley <nathan.mccauley@docker.com> Nathan McCauley <nathan.mccauley@docker.com>
Neil Peterson <neilpeterson@outlook.com> Neil Peterson <neilpeterson@outlook.com>
Nick Adcock <nick.adcock@docker.com> Nick Adcock <nick.adcock@docker.com>
Nick Santos <nick.santos@docker.com>
Nick Sieger <nick@nicksieger.com>
Nico Stapelbroek <nstapelbroek@gmail.com> Nico Stapelbroek <nstapelbroek@gmail.com>
Nicola Kabar <nicolaka@gmail.com> Nicola Kabar <nicolaka@gmail.com>
Nicolas Borboën <ponsfrilus@gmail.com> Nicolas Borboën <ponsfrilus@gmail.com>
@@ -680,58 +494,40 @@ Nicolas De Loof <nicolas.deloof@gmail.com>
Nikhil Chawla <chawlanikhil24@gmail.com> Nikhil Chawla <chawlanikhil24@gmail.com>
Nikolas Garofil <nikolas.garofil@uantwerpen.be> Nikolas Garofil <nikolas.garofil@uantwerpen.be>
Nikolay Milovanov <nmil@itransformers.net> Nikolay Milovanov <nmil@itransformers.net>
NinaLua <iturf@sina.cn>
Nir Soffer <nsoffer@redhat.com> Nir Soffer <nsoffer@redhat.com>
Nishant Totla <nishanttotla@gmail.com> Nishant Totla <nishanttotla@gmail.com>
NIWA Hideyuki <niwa.niwa@nifty.ne.jp> NIWA Hideyuki <niwa.niwa@nifty.ne.jp>
Noah Silas <noah@hustle.com>
Noah Treuhaft <noah.treuhaft@docker.com> Noah Treuhaft <noah.treuhaft@docker.com>
O.S. Tezer <ostezer@gmail.com> O.S. Tezer <ostezer@gmail.com>
Oded Arbel <oded@geek.co.il>
Odin Ugedal <odin@ugedal.com>
ohmystack <jun.jiang02@ele.me> ohmystack <jun.jiang02@ele.me>
OKA Naoya <git@okanaoya.com>
Oliver Pomeroy <oppomeroy@gmail.com>
Olle Jonsson <olle.jonsson@gmail.com> Olle Jonsson <olle.jonsson@gmail.com>
Olli Janatuinen <olli.janatuinen@gmail.com> Olli Janatuinen <olli.janatuinen@gmail.com>
Oscar Wieman <oscrx@icloud.com>
Otto Kekäläinen <otto@seravo.fi> Otto Kekäläinen <otto@seravo.fi>
Ovidio Mallo <ovidio.mallo@gmail.com> Ovidio Mallo <ovidio.mallo@gmail.com>
Park Jaeon <me@finalchild.dev>
Pascal Borreli <pascal@borreli.com> Pascal Borreli <pascal@borreli.com>
Patrick Böänziger <patrick.baenziger@bsi-software.com> Patrick Böänziger <patrick.baenziger@bsi-software.com>
Patrick Daigle <114765035+pdaig@users.noreply.github.com>
Patrick Hemmer <patrick.hemmer@gmail.com> Patrick Hemmer <patrick.hemmer@gmail.com>
Patrick Lang <plang@microsoft.com> Patrick Lang <plang@microsoft.com>
Patrick St. laurent <patrick@saint-laurent.us>
Paul <paul9869@gmail.com> Paul <paul9869@gmail.com>
Paul Kehrer <paul.l.kehrer@gmail.com> Paul Kehrer <paul.l.kehrer@gmail.com>
Paul Lietar <paul@lietar.net> Paul Lietar <paul@lietar.net>
Paul Mulders <justinkb@gmail.com>
Paul Rogalski <mail@paul-rogalski.de>
Paul Seyfert <pseyfert.mathphys@gmail.com>
Paul Weaver <pauweave@cisco.com> Paul Weaver <pauweave@cisco.com>
Pavel Pospisil <pospispa@gmail.com> Pavel Pospisil <pospispa@gmail.com>
Paweł Gronowski <pawel.gronowski@docker.com>
Paweł Pokrywka <pepawel@users.noreply.github.com>
Paweł Szczekutowicz <pszczekutowicz@gmail.com> Paweł Szczekutowicz <pszczekutowicz@gmail.com>
Peeyush Gupta <gpeeyush@linux.vnet.ibm.com> Peeyush Gupta <gpeeyush@linux.vnet.ibm.com>
Per Lundberg <perlun@gmail.com> Per Lundberg <per.lundberg@ecraft.com>
Peter Dave Hello <hsu@peterdavehello.org>
Peter Edge <peter.edge@gmail.com> Peter Edge <peter.edge@gmail.com>
Peter Hsu <shhsu@microsoft.com> Peter Hsu <shhsu@microsoft.com>
Peter Jaffe <pjaffe@nevo.com> Peter Jaffe <pjaffe@nevo.com>
Peter Kehl <peter.kehl@gmail.com> Peter Kehl <peter.kehl@gmail.com>
Peter Nagy <xificurC@gmail.com> Peter Nagy <xificurC@gmail.com>
Peter Salvatore <peter@psftw.com> Peter Salvatore <peter@psftw.com>
Peter Valdemar Mørch <peter@morch.com>
Peter Waller <p@pwaller.net> Peter Waller <p@pwaller.net>
Phil Estes <estesp@gmail.com> Phil Estes <estesp@linux.vnet.ibm.com>
Philip Alexander Etling <paetling@gmail.com> Philip Alexander Etling <paetling@gmail.com>
Philipp Gillé <philipp.gille@gmail.com> Philipp Gillé <philipp.gille@gmail.com>
Philipp Schmied <pschmied@schutzwerk.com> Philipp Schmied <pschmied@schutzwerk.com>
Phong Tran <tran.pho@northeastern.edu> pidster <pid@pidster.com>
Pieter E Smit <diepes@github.com>
pixelistik <pixelistik@users.noreply.github.com> pixelistik <pixelistik@users.noreply.github.com>
Pratik Karki <prertik@outlook.com> Pratik Karki <prertik@outlook.com>
Prayag Verma <prayag.verma@gmail.com> Prayag Verma <prayag.verma@gmail.com>
@@ -739,41 +535,30 @@ Preston Cowley <preston.cowley@sony.com>
Pure White <daniel48@126.com> Pure White <daniel48@126.com>
Qiang Huang <h.huangqiang@huawei.com> Qiang Huang <h.huangqiang@huawei.com>
Qinglan Peng <qinglanpeng@zju.edu.cn> Qinglan Peng <qinglanpeng@zju.edu.cn>
QQ喵 <gqqnb2005@gmail.com>
qudongfang <qudongfang@gmail.com> qudongfang <qudongfang@gmail.com>
Raghavendra K T <raghavendra.kt@linux.vnet.ibm.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> Ray Tsang <rayt@google.com>
Reficul <xuzhenglun@gmail.com> Reficul <xuzhenglun@gmail.com>
Remy Suen <remy.suen@gmail.com> Remy Suen <remy.suen@gmail.com>
Renaud Gaubert <rgaubert@nvidia.com> Renaud Gaubert <rgaubert@nvidia.com>
René Hermenau <rene-hermenau@users.noreply.github.com>
Ricardo N Feliciano <FelicianoTech@gmail.com> Ricardo N Feliciano <FelicianoTech@gmail.com>
Rich Moyse <rich@moyse.us> Rich Moyse <rich@moyse.us>
Richard Chen Zheng <58443436+rchenzheng@users.noreply.github.com>
Richard Mathie <richard.mathie@amey.co.uk> Richard Mathie <richard.mathie@amey.co.uk>
Richard Scothern <richard.scothern@gmail.com> Richard Scothern <richard.scothern@gmail.com>
Rick Wieman <git@rickw.nl> Rick Wieman <git@rickw.nl>
Ritesh H Shukla <sritesh@vmware.com> Ritesh H Shukla <sritesh@vmware.com>
Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com> Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
Rob Gulewich <rgulewich@netflix.com>
Rob Murray <rob.murray@docker.com>
Robert Wallis <smilingrob@gmail.com> Robert Wallis <smilingrob@gmail.com>
Robin Naundorf <r.naundorf@fh-muenster.de> Robin Naundorf <r.naundorf@fh-muenster.de>
Robin Speekenbrink <robin@kingsquare.nl> Robin Speekenbrink <robin@kingsquare.nl>
Roch Feuillade <roch.feuillade@pandobac.com>
Rodolfo Ortiz <rodolfo.ortiz@definityfirst.com> Rodolfo Ortiz <rodolfo.ortiz@definityfirst.com>
Rogelio Canedo <rcanedo@mappy.priv> Rogelio Canedo <rcanedo@mappy.priv>
Rohan Verma <hello@rohanverma.net>
Roland Kammerer <roland.kammerer@linbit.com> Roland Kammerer <roland.kammerer@linbit.com>
Roman Dudin <katrmr@gmail.com> Roman Dudin <katrmr@gmail.com>
Rory Hunter <roryhunter2@gmail.com> Rory Hunter <roryhunter2@gmail.com>
Ross Boucher <rboucher@gmail.com> Ross Boucher <rboucher@gmail.com>
Rubens Figueiredo <r.figueiredo.52@gmail.com> Rubens Figueiredo <r.figueiredo.52@gmail.com>
Rui Cao <ruicao@alauda.io> Rui Cao <ruicao@alauda.io>
Rui JingAn <quiterace@gmail.com>
Ryan Belgrave <rmb1993@gmail.com> Ryan Belgrave <rmb1993@gmail.com>
Ryan Detzel <ryan.detzel@gmail.com> Ryan Detzel <ryan.detzel@gmail.com>
Ryan Stelly <ryan.stelly@live.com> Ryan Stelly <ryan.stelly@live.com>
@@ -783,24 +568,15 @@ Sainath Grandhi <sainath.grandhi@intel.com>
Sakeven Jiang <jc5930@sina.cn> Sakeven Jiang <jc5930@sina.cn>
Sally O'Malley <somalley@redhat.com> Sally O'Malley <somalley@redhat.com>
Sam Neirinck <sam@samneirinck.com> Sam Neirinck <sam@samneirinck.com>
Sam Thibault <sam.thibault@docker.com>
Samarth Shah <samashah@microsoft.com>
Sambuddha Basu <sambuddhabasu1@gmail.com> Sambuddha Basu <sambuddhabasu1@gmail.com>
Sami Tabet <salph.tabet@gmail.com> Sami Tabet <salph.tabet@gmail.com>
Samuel Cochran <sj26@sj26.com>
Samuel Karp <skarp@amazon.com> Samuel Karp <skarp@amazon.com>
Sandro Jäckel <sandro.jaeckel@gmail.com>
Santhosh Manohar <santhosh@docker.com> Santhosh Manohar <santhosh@docker.com>
Sarah Sanders <sarah.sanders@docker.com>
Sargun Dhillon <sargun@netflix.com>
Saswat Bhattacharya <sas.saswat@gmail.com>
Saurabh Kumar <saurabhkumar0184@gmail.com>
Scott Brenner <scott@scottbrenner.me> Scott Brenner <scott@scottbrenner.me>
Scott Collier <emailscottcollier@gmail.com> Scott Collier <emailscottcollier@gmail.com>
Sean Christopherson <sean.j.christopherson@intel.com> Sean Christopherson <sean.j.christopherson@intel.com>
Sean Rodman <srodman7689@gmail.com> Sean Rodman <srodman7689@gmail.com>
Sebastiaan van Stijn <github@gone.nl> Sebastiaan van Stijn <github@gone.nl>
Seiya Miyata <odradek38@gmail.com>
Sergey Tryuber <Sergeant007@users.noreply.github.com> Sergey Tryuber <Sergeant007@users.noreply.github.com>
Serhat Gülçiçek <serhat25@gmail.com> Serhat Gülçiçek <serhat25@gmail.com>
Sevki Hasirci <s@sevki.org> Sevki Hasirci <s@sevki.org>
@@ -816,16 +592,13 @@ sidharthamani <sid@rancher.com>
Silvin Lubecki <silvin.lubecki@docker.com> Silvin Lubecki <silvin.lubecki@docker.com>
Simei He <hesimei@zju.edu.cn> Simei He <hesimei@zju.edu.cn>
Simon Ferquel <simon.ferquel@docker.com> Simon Ferquel <simon.ferquel@docker.com>
Simon Heimberg <simon.heimberg@heimberg-ea.ch>
Sindhu S <sindhus@live.in> Sindhu S <sindhus@live.in>
Slava Semushin <semushin@redhat.com> Slava Semushin <semushin@redhat.com>
Solomon Hykes <solomon@docker.com> Solomon Hykes <solomon@docker.com>
Song Gao <song@gao.io> Song Gao <song@gao.io>
Spencer Brown <spencer@spencerbrown.org> Spencer Brown <spencer@spencerbrown.org>
Spring Lee <xi.shuai@outlook.com> squeegels <1674195+squeegels@users.noreply.github.com>
squeegels <lmscrewy@gmail.com>
Srini Brahmaroutu <srbrahma@us.ibm.com> Srini Brahmaroutu <srbrahma@us.ibm.com>
Stavros Panakakis <stavrospanakakis@gmail.com>
Stefan S. <tronicum@user.github.com> Stefan S. <tronicum@user.github.com>
Stefan Scherer <stefan.scherer@docker.com> Stefan Scherer <stefan.scherer@docker.com>
Stefan Weil <sw@weilnetz.de> Stefan Weil <sw@weilnetz.de>
@@ -835,8 +608,6 @@ Stephen Rust <srust@blockbridge.com>
Steve Durrheimer <s.durrheimer@gmail.com> Steve Durrheimer <s.durrheimer@gmail.com>
Steve Richards <steve.richards@docker.com> Steve Richards <steve.richards@docker.com>
Steven Burgess <steven.a.burgess@hotmail.com> Steven Burgess <steven.a.burgess@hotmail.com>
Stoica-Marcu Floris-Andrei <floris.sm@gmail.com>
Stuart Williams <pid@pidster.com>
Subhajit Ghosh <isubuz.g@gmail.com> Subhajit Ghosh <isubuz.g@gmail.com>
Sun Jianbo <wonderflow.sun@gmail.com> Sun Jianbo <wonderflow.sun@gmail.com>
Sune Keller <absukl@almbrand.dk> Sune Keller <absukl@almbrand.dk>
@@ -848,15 +619,9 @@ Sébastien HOUZÉ <cto@verylastroom.com>
T K Sourabh <sourabhtk37@gmail.com> T K Sourabh <sourabhtk37@gmail.com>
TAGOMORI Satoshi <tagomoris@gmail.com> TAGOMORI Satoshi <tagomoris@gmail.com>
taiji-tech <csuhqg@foxmail.com> taiji-tech <csuhqg@foxmail.com>
Takeshi Koenuma <t.koenuma2@gmail.com>
Takuya Noguchi <takninnovationresearch@gmail.com>
Taylor Jones <monitorjbl@gmail.com> Taylor Jones <monitorjbl@gmail.com>
Teiva Harsanyi <t.harsanyi@thebeat.co>
Tejaswini Duggaraju <naduggar@microsoft.com> Tejaswini Duggaraju <naduggar@microsoft.com>
Tengfei Wang <tfwang@alauda.io>
Teppei Fukuda <knqyf263@gmail.com>
Thatcher Peskens <thatcher@docker.com> Thatcher Peskens <thatcher@docker.com>
Thibault Coupin <thibault.coupin@gmail.com>
Thomas Gazagnaire <thomas@gazagnaire.org> Thomas Gazagnaire <thomas@gazagnaire.org>
Thomas Krzero <thomas.kovatchitch@gmail.com> Thomas Krzero <thomas.kovatchitch@gmail.com>
Thomas Leonard <thomas.leonard@docker.com> Thomas Leonard <thomas.leonard@docker.com>
@@ -868,10 +633,8 @@ Tianyi Wang <capkurmagati@gmail.com>
Tibor Vass <teabee89@gmail.com> Tibor Vass <teabee89@gmail.com>
Tim Dettrick <t.dettrick@uq.edu.au> Tim Dettrick <t.dettrick@uq.edu.au>
Tim Hockin <thockin@google.com> Tim Hockin <thockin@google.com>
Tim Sampson <tim@sampson.fi>
Tim Smith <timbot@google.com> Tim Smith <timbot@google.com>
Tim Waugh <twaugh@redhat.com> Tim Waugh <twaugh@redhat.com>
Tim Welsh <timothy.welsh@docker.com>
Tim Wraight <tim.wraight@tangentlabs.co.uk> Tim Wraight <tim.wraight@tangentlabs.co.uk>
timfeirg <kkcocogogo@gmail.com> timfeirg <kkcocogogo@gmail.com>
Timothy Hobbs <timothyhobbs@seznam.cz> Timothy Hobbs <timothyhobbs@seznam.cz>
@@ -883,7 +646,6 @@ Tom Fotherby <tom+github@peopleperhour.com>
Tom Klingenberg <tklingenberg@lastflood.net> Tom Klingenberg <tklingenberg@lastflood.net>
Tom Milligan <code@tommilligan.net> Tom Milligan <code@tommilligan.net>
Tom X. Tobin <tomxtobin@tomxtobin.com> Tom X. Tobin <tomxtobin@tomxtobin.com>
Tomas Bäckman <larstomas@gmail.com>
Tomas Tomecek <ttomecek@redhat.com> Tomas Tomecek <ttomecek@redhat.com>
Tomasz Kopczynski <tomek@kopczynski.net.pl> Tomasz Kopczynski <tomek@kopczynski.net.pl>
Tomáš Hrčka <thrcka@redhat.com> Tomáš Hrčka <thrcka@redhat.com>
@@ -895,26 +657,18 @@ Tristan Carel <tristan@cogniteev.com>
Tycho Andersen <tycho@docker.com> Tycho Andersen <tycho@docker.com>
Tycho Andersen <tycho@tycho.ws> Tycho Andersen <tycho@tycho.ws>
uhayate <uhayate.gong@daocloud.io> uhayate <uhayate.gong@daocloud.io>
Ulrich Bareth <ulrich.bareth@gmail.com>
Ulysses Souza <ulysses.souza@docker.com> Ulysses Souza <ulysses.souza@docker.com>
Umesh Yadav <umesh4257@gmail.com> Umesh Yadav <umesh4257@gmail.com>
Vaclav Struhar <struharv@gmail.com>
Valentin Lorentz <progval+git@progval.net> Valentin Lorentz <progval+git@progval.net>
Vardan Pogosian <vardan.pogosyan@gmail.com>
Varun Hotani <varunhotani@gmail.com>
Venkateswara Reddy Bukkasamudram <bukkasamudram@outlook.com>
Veres Lajos <vlajos@gmail.com> Veres Lajos <vlajos@gmail.com>
Vibhu Anan <vibhuanand@outlook.com>
Victor Vieux <victor.vieux@docker.com> Victor Vieux <victor.vieux@docker.com>
Victoria Bialas <victoria.bialas@docker.com> Victoria Bialas <victoria.bialas@docker.com>
Viktor Stanchev <me@viktorstanchev.com> Viktor Stanchev <me@viktorstanchev.com>
Ville Skyttä <ville.skytta@iki.fi>
Vimal Raghubir <vraghubir0418@gmail.com> Vimal Raghubir <vraghubir0418@gmail.com>
Vincent Batts <vbatts@redhat.com> Vincent Batts <vbatts@redhat.com>
Vincent Bernat <Vincent.Bernat@exoscale.ch> Vincent Bernat <Vincent.Bernat@exoscale.ch>
Vincent Demeester <vincent.demeester@docker.com> Vincent Demeester <vincent.demeester@docker.com>
Vincent Woo <me@vincentwoo.com> Vincent Woo <me@vincentwoo.com>
Vineet Kumar <vineetkumar17112004@gmail.com>
Vishnu Kannan <vishnuk@google.com> Vishnu Kannan <vishnuk@google.com>
Vivek Goyal <vgoyal@redhat.com> Vivek Goyal <vgoyal@redhat.com>
Wang Jie <wangjie5@chinaskycloud.com> Wang Jie <wangjie5@chinaskycloud.com>
@@ -923,18 +677,14 @@ Wang Long <long.wanglong@huawei.com>
Wang Ping <present.wp@icloud.com> Wang Ping <present.wp@icloud.com>
Wang Xing <hzwangxing@corp.netease.com> Wang Xing <hzwangxing@corp.netease.com>
Wang Yuexiao <wang.yuexiao@zte.com.cn> Wang Yuexiao <wang.yuexiao@zte.com.cn>
Wang Yumu <37442693@qq.com>
Wataru Ishida <ishida.wataru@lab.ntt.co.jp> Wataru Ishida <ishida.wataru@lab.ntt.co.jp>
Wayne Song <wsong@docker.com> Wayne Song <wsong@docker.com>
Wen Cheng Ma <wenchma@cn.ibm.com> Wen Cheng Ma <wenchma@cn.ibm.com>
Wenlong Zhang <zhangwenlong@loongson.cn>
Wenzhi Liang <wenzhi.liang@gmail.com> Wenzhi Liang <wenzhi.liang@gmail.com>
Wes Morgan <cap10morgan@gmail.com> Wes Morgan <cap10morgan@gmail.com>
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn> Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
Will Wang <willww64@gmail.com>
William Henry <whenry@redhat.com> William Henry <whenry@redhat.com>
Xianglin Gao <xlgao@zju.edu.cn> Xianglin Gao <xlgao@zju.edu.cn>
Xiaodong Liu <liuxiaodong@loongson.cn>
Xiaodong Zhang <a4012017@sina.com> Xiaodong Zhang <a4012017@sina.com>
Xiaoxi He <xxhe@alauda.io> Xiaoxi He <xxhe@alauda.io>
Xinbo Weng <xihuanbo_0521@zju.edu.cn> Xinbo Weng <xihuanbo_0521@zju.edu.cn>
@@ -944,31 +694,23 @@ Yanqiang Miao <miao.yanqiang@zte.com.cn>
Yassine Tijani <yasstij11@gmail.com> Yassine Tijani <yasstij11@gmail.com>
Yi EungJun <eungjun.yi@navercorp.com> Yi EungJun <eungjun.yi@navercorp.com>
Ying Li <ying.li@docker.com> Ying Li <ying.li@docker.com>
Yoan Wainmann <thebook90yw@gmail.com>
Yong Tang <yong.tang.github@outlook.com> Yong Tang <yong.tang.github@outlook.com>
Yosef Fertel <yfertel@gmail.com> Yosef Fertel <yfertel@gmail.com>
Yu Peng <yu.peng36@zte.com.cn> Yu Peng <yu.peng36@zte.com.cn>
Yuan Sun <sunyuan3@huawei.com> Yuan Sun <sunyuan3@huawei.com>
Yucheng Wu <wyc123wyc@gmail.com>
Yue Zhang <zy675793960@yeah.net> Yue Zhang <zy675793960@yeah.net>
Yunxiang Huang <hyxqshk@vip.qq.com> Yunxiang Huang <hyxqshk@vip.qq.com>
Zachary Romero <zacromero3@gmail.com> Zachary Romero <zacromero3@gmail.com>
Zander Mackie <zmackie@gmail.com>
zebrilee <zebrilee@gmail.com> zebrilee <zebrilee@gmail.com>
Zeel B Patel <patel_zeel@iitgn.ac.in>
Zhang Kun <zkazure@gmail.com> Zhang Kun <zkazure@gmail.com>
Zhang Wei <zhangwei555@huawei.com> Zhang Wei <zhangwei555@huawei.com>
Zhang Wentao <zhangwentao234@huawei.com> Zhang Wentao <zhangwentao234@huawei.com>
ZhangHang <stevezhang2014@gmail.com> ZhangHang <stevezhang2014@gmail.com>
zhenghenghuo <zhenghenghuo@zju.edu.cn> zhenghenghuo <zhenghenghuo@zju.edu.cn>
Zhiwei Liang <zliang@akamai.com>
Zhou Hao <zhouhao@cn.fujitsu.com> Zhou Hao <zhouhao@cn.fujitsu.com>
Zhoulin Xie <zhoulin.xie@daocloud.io> Zhoulin Xie <zhoulin.xie@daocloud.io>
Zhu Guihua <zhugh.fnst@cn.fujitsu.com> Zhu Guihua <zhugh.fnst@cn.fujitsu.com>
Zhuo Zhi <h.dwwwwww@gmail.com>
Álex González <agonzalezro@gmail.com> Álex González <agonzalezro@gmail.com>
Álvaro Lázaro <alvaro.lazaro.g@gmail.com> Álvaro Lázaro <alvaro.lazaro.g@gmail.com>
Átila Camurça Alves <camurca.home@gmail.com> Átila Camurça Alves <camurca.home@gmail.com>
Александр Менщиков <__Singleton__@hackerdom.ru>
徐俊杰 <paco.xu@daocloud.io> 徐俊杰 <paco.xu@daocloud.io>
林博仁 Buo-ren Lin <Buo.Ren.Lin@gmail.com>
+27 -57
View File
@@ -1,5 +1,9 @@
# Contributing to Docker # 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 This page contains information about reporting issues as well as some tips and
guidelines useful to experienced open source contributors. Finally, make sure guidelines useful to experienced open source contributors. Finally, make sure
you read our [community guidelines](#docker-community-guidelines) before you you read our [community guidelines](#docker-community-guidelines) before you
@@ -16,9 +20,9 @@ start participating.
## Reporting security issues ## Reporting security issues
The Docker maintainers take security seriously. If you discover a security 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@docker.com](mailto:security@docker.com).
Security reports are greatly appreciated and we will publicly thank you for it. 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 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 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 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: When reporting issues, always include:
@@ -66,7 +70,7 @@ anybody starts working on it.
We are always thrilled to receive pull requests. We do our best to process them We are always thrilled to receive pull requests. We do our best to process them
quickly. If your pull request is not accepted on the first try, quickly. If your pull request is not accepted on the first try,
don't get discouraged! Our contributor's guide explains [the review process we don't get discouraged! Our contributor's guide explains [the review process we
use for simple changes](https://github.com/moby/moby/blob/master/project/REVIEWING.md). use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contribution/).
### Talking to other Docker users and contributors ### Talking to other Docker users and contributors
@@ -84,7 +88,7 @@ use for simple changes](https://github.com/moby/moby/blob/master/project/REVIEWI
<tr> <tr>
<td>Community Slack</td> <td>Community Slack</td>
<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> </td>
</tr> </tr>
<tr> <tr>
@@ -124,7 +128,8 @@ submitting a pull request.
Update the documentation when creating or modifying features. Test your Update the documentation when creating or modifying features. Test your
documentation changes for clarity, concision, and correctness, as well as a documentation changes for clarity, concision, and correctness, as well as a
clean documentation build. See our contributors guide for [our style clean documentation build. See our contributors guide for [our style
guide](https://github.com/docker/docs/blob/main/STYLE.md). guide](https://docs.docker.com/opensource/doc-style) and instructions on [building
the documentation](https://docs.docker.com/opensource/project/test-and-docs/#build-and-test-the-documentation).
Write clean code. Universally formatted code promotes ease of writing, reading, Write clean code. Universally formatted code promotes ease of writing, reading,
and maintenance. Always run `gofmt -s -w file.go` on each changed file before and maintenance. Always run `gofmt -s -w file.go` on each changed file before
@@ -133,45 +138,9 @@ committing your changes. Most editors have plug-ins that do this automatically.
Pull request descriptions should be as clear as possible and include a reference Pull request descriptions should be as clear as possible and include a reference
to all the issues that they address. to all the issues that they address.
Commit messages must be written in the imperative mood (max. 72 chars), followed Commit messages must start with a capitalized and short summary (max. 50 chars)
by an optional, more detailed explanatory text usually expanding on written in the imperative, followed by an optional, more detailed explanatory
why the work is necessary. The explanatory text should be separated by an text which is separated from the summary by an empty line.
empty line.
The commit message *could* have a prefix scoping the change, however this is
not enforced. Common prefixes are `docs: <message>`, `vendor: <message>`,
`chore: <message>` or the package/area related to the change such as `pkg/foo: <message>`
or `telemetry: <message>`.
A standard commit.
```
Fix the exploding flux capacitor
A call to function A causes the flux capacitor to blow up every time
the sun and the moon align.
```
Using a package as prefix.
```
pkg/foo: prevent panic in flux capacitor
Calling function A causes the flux capacitor to blow up every time
the sun and the moon align.
```
Updating a specific vendored dependency.
```
vendor: github.com/moby/moby/client v0.4.0
```
Fixing a broken docs link.
```
docs: fix style/lint issues in deprecated.md
```
Code review comments may be added to your pull request. Discuss, then make the Code review comments may be added to your pull request. Discuss, then make the
suggested modifications and push additional commits to your feature branch. Post suggested modifications and push additional commits to your feature branch. Post
@@ -201,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 description that close an issue. Including references automatically closes the issue
on a merge. 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. 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 ### Merge approval
@@ -223,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 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 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 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 Developer Certificate of Origin
@@ -267,7 +236,7 @@ Then you just add a line to every git commit message:
Signed-off-by: Joe Smith <joe.smith@email.com> Signed-off-by: Joe Smith <joe.smith@email.com>
Use a known identity (sorry, no anonymous contributions.) Use your real name (sorry, no pseudonyms or anonymous contributions.)
If you set your `user.name` and `user.email` git configs, you can sign your If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`. commit automatically with `git commit -s`.
@@ -304,8 +273,8 @@ guidelines for the community as a whole:
* Stay on topic: Make sure that you are posting to the correct channel and * 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 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 to an email you are potentially sending to a large number of people. Please
this before you update. Also remember that nobody likes spam. 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 * 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 maintainers to ask them to investigate an issue or to take a look at a
@@ -364,11 +333,12 @@ mind when nudging others to comply.
The rules: 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 2. All code should pass the default levels of
[`golint`](https://github.com/golang/lint). [`golint`](https://github.com/golang/lint).
3. All code should follow the guidelines covered in [Effective Go](https://go.dev/doc/effective_go) 3. All code should follow the guidelines covered in [Effective
and [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments). 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. 4. Comment the code. Tell us the why, the history and the context.
5. Document _all_ declarations and methods, even private ones. Declare 5. Document _all_ declarations and methods, even private ones. Declare
expectations, caveats and anything else that may be important. If a type expectations, caveats and anything else that may be important. If a type
@@ -390,6 +360,6 @@ The rules:
guidelines. Since you've read all the rules, you now know that. 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 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 reading through [Effective Go](https://golang.org/doc/effective_go.html). The
[Go Blog](https://go.dev/blog/) is also a great resource. Drinking the [Go Blog](https://blog.golang.org) is also a great resource. Drinking the
kool-aid is a lot easier than going thirsty. kool-aid is a lot easier than going thirsty.
-151
View File
@@ -1,151 +0,0 @@
# syntax=docker/dockerfile:1
ARG BASE_VARIANT=alpine
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
# that's also available as alpine image variant for the Golang version used.
ARG ALPINE_VERSION=3.23
ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.26.8
# XX_VERSION specifies the version of the xx utility to use.
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
ARG XX_VERSION=1.9.0
# GOVERSIONINFO_VERSION is the version of GoVersionInfo to install.
# It must be a valid tag from https://github.com/josephspurrier/goversioninfo
ARG GOVERSIONINFO_VERSION=v1.5.0
# GOTESTSUM_VERSION sets the version of gotestsum to install in the dev container.
# It must be a valid tag in the https://github.com/gotestyourself/gotestsum repository.
ARG GOTESTSUM_VERSION=v1.13.0
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
# It must be a tag in the docker.io/docker/buildx-bin image repository
# on Docker Hub.
ARG BUILDX_VERSION=0.34.1
# COMPOSE_VERSION is the version of compose to install in the dev container.
# It must be a tag in the docker.io/docker/compose-bin image repository
# on Docker Hub.
ARG COMPOSE_VERSION=v5.1.3
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 git-daemon
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=cmd/docker/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/|/cmd/docker-trust')
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
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 .
Vendored
+13
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"
}
}
+21 -44
View File
@@ -22,9 +22,11 @@
# subsystem maintainers accountable. If ownership is unclear, they are the de facto owners. # subsystem maintainers accountable. If ownership is unclear, they are the de facto owners.
people = [ people = [
"aaronlehmann",
"albers", "albers",
"cpuguy83", "cpuguy83",
"rumpl", "dnephin",
"justincormack",
"silvin-lubecki", "silvin-lubecki",
"stevvooe", "stevvooe",
"thajeztah", "thajeztah",
@@ -34,6 +36,14 @@
"vieux", "vieux",
] ]
[Org."Docs maintainers"]
# TODO Describe the docs maintainers role.
people = [
"thajeztah"
]
[Org.Curators] [Org.Curators]
# The curators help ensure that incoming issues and pull requests are properly triaged and # 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 # - close an issue or pull request when it's inappropriate or off-topic
people = [ people = [
"bsousaa",
"neersighted",
"programmerq", "programmerq",
"sam-thibault", "thajeztah"
"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",
] ]
[people] [people]
@@ -77,16 +69,16 @@
# ADD YOURSELF HERE IN ALPHABETICAL ORDER # ADD YOURSELF HERE IN ALPHABETICAL ORDER
[people.aaronlehmann]
Name = "Aaron Lehmann"
Email = "aaron.lehmann@docker.com"
GitHub = "aaronlehmann"
[people.albers] [people.albers]
Name = "Harald Albers" Name = "Harald Albers"
Email = "github@albersweb.de" Email = "github@albersweb.de"
GitHub = "albers" GitHub = "albers"
[people.bsousaa]
Name = "Bruno de Sousa"
Email = "bruno.sousa@docker.com"
GitHub = "bsousaa"
[people.cpuguy83] [people.cpuguy83]
Name = "Brian Goff" Name = "Brian Goff"
Email = "cpuguy83@gmail.com" Email = "cpuguy83@gmail.com"
@@ -97,26 +89,16 @@
Email = "dnephin@gmail.com" Email = "dnephin@gmail.com"
GitHub = "dnephin" GitHub = "dnephin"
[people.neersighted] [people.justincormack]
Name = "Bjorn Neergaard" Name = "Justin Cormack"
Email = "bneergaard@mirantis.com" Email = "justin.cormack@docker.com"
GitHub = "neersighted" GitHub = "justincormack"
[people.programmerq] [people.programmerq]
Name = "Jeff Anderson" Name = "Jeff Anderson"
Email = "jeff@docker.com" Email = "jeff@docker.com"
GitHub = "programmerq" 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] [people.silvin-lubecki]
Name = "Silvin Lubecki" Name = "Silvin Lubecki"
Email = "silvin.lubecki@docker.com" Email = "silvin.lubecki@docker.com"
@@ -152,8 +134,3 @@
Email = "vieux@docker.com" Email = "vieux@docker.com"
GitHub = "vieux" GitHub = "vieux"
[people.vvoland]
Name = "Paweł Gronowski"
Email = "pawel.gronowski@docker.com"
GitHub = "vvoland"
+59 -96
View File
@@ -1,139 +1,102 @@
# #
# github.com/docker/cli # 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 all: binary
_:=$(shell ./scripts/warn-outside-container $(MAKECMDGOALS)) _:=$(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 .PHONY: clean
clean: ## remove build artifacts 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 .PHONY: test
test: test-unit ## run tests test: test-unit ## run tests
.PHONY: test-unit
test-unit: ## run unit tests, to change the output format use: GOTESTSUM_FORMAT=(dots|short|standard-quiet|short-verbose|standard-verbose) make test-unit
gotestsum -- $${TESTDIRS:-$(shell go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust')} $(TESTFLAGS)
.PHONY: test-coverage .PHONY: test-coverage
test-coverage: ## run test coverage test-coverage: ## run test coverage
mkdir -p $(CURDIR)/build/coverage gotestsum -- -coverprofile=coverage.txt $(shell go list ./... | grep -vE '/vendor/|/e2e/')
gotestsum -- $(shell go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust') -coverprofile=$(CURDIR)/build/coverage/coverage.txt
.PHONY: fmt
fmt:
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d
.PHONY: lint .PHONY: lint
lint: ## run all the lint tools lint: ## run all the lint tools
golangci-lint run gometalinter --config gometalinter.json ./...
.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.24 . ; \
else \
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \
fi
.PHONY: binary .PHONY: binary
binary: ## build executable for Linux binary: ## build executable for Linux
@echo "WARNING: binary creates a Linux executable. Use cross for macOS or Windows."
./scripts/build/binary ./scripts/build/binary
.PHONY: dynbinary
dynbinary: ## build dynamically linked binary
GO_LINKMODE=dynamic ./scripts/build/binary
.PHONY: plugins .PHONY: plugins
plugins: ## build example CLI plugins plugins: ## build example CLI plugins
scripts/build/plugins ./scripts/build/plugins
.PHONY: trust-plugin .PHONY: cross
trust-plugin: ## build docker-trust CLI plugins cross: ## build executable for macOS and Windows
scripts/build/trust-plugin ./scripts/build/cross
.PHONY: install-trust-plugin .PHONY: binary-windows
install-trust-plugin: trust-plugin binary-windows: ## build executable for Windows
install-trust-plugin: ## install docker-trust CLI plugins ./scripts/build/windows
install -D -m 0755 "$$(readlink -f build/docker-trust)" /usr/libexec/docker/cli-plugins/docker-trust
.PHONY: vendor .PHONY: plugins-windows
vendor: ## update vendor with go modules 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 rm -rf vendor
scripts/with-go-mod.sh scripts/vendor update bash -c 'vndr |& grep -v -i clone'
scripts/validate/check-git-diff vendor
.PHONY: validate-vendor
validate-vendor: ## validate vendor
scripts/with-go-mod.sh scripts/vendor validate
.PHONY: mod-outdated
mod-outdated: ## check outdated dependencies
scripts/with-go-mod.sh scripts/vendor outdated
.PHONY: authors .PHONY: authors
authors: ## generate AUTHORS file from git history authors: ## generate AUTHORS file from git history
scripts/docs/generate-authors.sh scripts/docs/generate-authors.sh
.PHONY: completion
completion: shell-completion
completion: ## generate and install the shell-completion scripts
# Note: this uses system-wide paths, and so may overwrite completion
# scripts installed as part of deb/rpm packages.
#
# Given that this target is intended to debug/test updated versions, we could
# consider installing in per-user (~/.config, XDG_DATA_DIR) paths instead, but
# this will add more complexity.
#
# See https://github.com/docker/cli/pull/5770#discussion_r1927772710
install -D -p -m 0644 ./build/completion/bash/docker /usr/share/bash-completion/completions/docker
install -D -p -m 0644 ./build/completion/fish/docker.fish debian/docker-ce-cli/usr/share/fish/vendor_completions.d/docker.fish
install -D -p -m 0644 ./build/completion/zsh/_docker debian/docker-ce-cli/usr/share/zsh/vendor-completions/_docker
build/docker:
# This target is used by the "shell-completion" target, which requires either
# "binary" or "dynbinary" to have been built. We don't want to trigger those
# to prevent replacing a static binary with a dynamic one, or vice-versa.
@echo "Run 'make binary' or 'make dynbinary' first" && exit 1
.PHONY: shell-completion
shell-completion: build/docker # requires either "binary" or "dynbinary" to be built.
shell-completion: ## generate shell-completion scripts
@ ./scripts/build/shell-completion
.PHONY: manpages .PHONY: manpages
manpages: ## generate man pages from go source and markdown manpages: ## generate man pages from go source and markdown
scripts/with-go-mod.sh scripts/docs/generate-man.sh scripts/docs/generate-man.sh
.PHONY: mddocs
mddocs: ## generate markdown files from go source
scripts/with-go-mod.sh scripts/docs/generate-md.sh
.PHONY: yamldocs .PHONY: yamldocs
yamldocs: ## generate documentation YAML files consumed by docs repo yamldocs: ## generate documentation YAML files consumed by docs repo
scripts/with-go-mod.sh scripts/docs/generate-yaml.sh scripts/docs/generate-yaml.sh
.PHONY: shellcheck
shellcheck: ## run shellcheck validation
scripts/validate/shellcheck
.PHONY: help .PHONY: help
help: ## print this 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) @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
+2 -2
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 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. by Keith Rarick, licensed under the MIT License.
The following is courtesy of our legal counsel: 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 It is your responsibility to ensure that your use and/or transfer does not
violate applicable laws. 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. See also https://www.apache.org/dev/crypto.html and/or seek legal counsel.
+27 -55
View File
@@ -1,86 +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://pkg.go.dev/badge/github.com/docker/cli)](https://pkg.go.dev/github.com/docker/cli) 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)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/docker/cli/badge)](https://scorecard.dev/viewer/?uri=github.com/docker/cli)
[![Codecov](https://img.shields.io/codecov/c/github/docker/cli?logo=codecov)](https://codecov.io/gh/docker/cli)
[![Protected by Tracebit Community Edition](https://github.com/tracebit-com/tracebit-community-action/blob/main/assets/badges/tracebit-badge-compact.svg?raw=1)](https://community.tracebit.com/supplychainattacks)
## 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. `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: Build binaries for all supported platforms:
```shell
docker buildx bake cross
``` ```
$ make -f docker.Makefile 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
``` ```
Run all linting: Run all linting:
```shell
docker buildx bake lint shellcheck
``` ```
$ make -f docker.Makefile lint
Run test:
```shell
docker buildx bake test
```
Run the unit test:
```
$ make -f docker.Makefile test-unit
```
Run the full test suite:
```
$ make -f docker.Makefile test
``` ```
List all the available targets: List all the available targets:
```shell ```
make help $ make help
``` ```
### In-container development environment ### In-container development environment
Start an interactive development environment: Start an interactive development environment:
```shell ```
make 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, *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 Use and transfer of Docker may be subject to certain restrictions by the
United States and other governments. United States and other governments.
@@ -88,10 +60,10 @@ United States and other governments.
It is your responsibility to ensure that your use and/or transfer does not It is your responsibility to ensure that your use and/or transfer does not
violate applicable laws. violate applicable laws.
For more information, see https://www.bis.doc.gov For more information, please see https://www.bis.doc.gov
## Licensing
Licensing
=========
docker/cli is licensed under the Apache License, Version 2.0. See 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](https://github.com/docker/docker/blob/master/LICENSE) for the full
license text. license text.
-44
View File
@@ -1,44 +0,0 @@
# Security Policy
The maintainers of the Docker CLI take security seriously. If you discover
a security issue, please bring it to their attention right away!
## Reporting a Vulnerability
Please **DO NOT** file a public issue, instead send your report privately
to [security@docker.com](mailto:security@docker.com).
Reporter(s) can expect a response within 72 hours, acknowledging the issue was
received.
## Review Process
After receiving the report, an initial triage and technical analysis is
performed to confirm the report and determine its scope. We may request
additional information in this stage of the process.
Once a reviewer has confirmed the relevance of the report, a draft security
advisory will be created on GitHub. The draft advisory will be used to discuss
the issue with maintainers, the reporter(s), and where applicable, other
affected parties under embargo.
If the vulnerability is accepted, a timeline for developing a patch, public
disclosure, and patch release will be determined. If there is an embargo period
on public disclosure before the patch release, the reporter(s) are expected to
participate in the discussion of the timeline and abide by agreed upon dates
for public disclosure.
## Accreditation
Security reports are greatly appreciated and we will publicly thank you,
although we will keep your name confidential if you request it. We also like to
send gifts - if you're into swag, make sure to let us know. We do not currently
offer a paid security bounty program at this time.
## Supported Versions
This project uses long-lived branches to maintain releases, and follows
the maintenance cycle of the Moby project.
Refer to [BRANCHES-AND-TAGS.md](https://github.com/moby/moby/blob/master/project/BRANCHES-AND-TAGS.md)
in the default branch of the moby repository to learn about the current
maintenance status of each branch.
+1 -1
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. 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>` 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 additional fixture images to the registry see
[scripts/test/e2e/run](https://github.com/docker/cli/blob/master/scripts/test/e2e/run). [scripts/test/e2e/run](https://github.com/docker/cli/blob/master/scripts/test/e2e/run).
+1 -1
View File
@@ -1 +1 @@
29.8.2 19.03.0-dev
+23
View File
@@ -0,0 +1,23 @@
version: "{build}"
clone_folder: c:\gopath\src\github.com\docker\cli
environment:
GOPATH: c:\gopath
GOVERSION: 1.12.8
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
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
+20 -20
View File
@@ -5,32 +5,31 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/moby/moby/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func main() { func main() {
plugin.Run(func(dockerCLI command.Cli) *cobra.Command { plugin.Run(func(dockerCli command.Cli) *cobra.Command {
goodbye := &cobra.Command{ goodbye := &cobra.Command{
Use: "goodbye", Use: "goodbye",
Short: "Say Goodbye instead of Hello", Short: "Say Goodbye instead of Hello",
Run: func(cmd *cobra.Command, _ []string) { Run: func(cmd *cobra.Command, _ []string) {
_, _ = fmt.Fprintln(dockerCLI.Out(), "Goodbye World!") fmt.Fprintln(dockerCli.Out(), "Goodbye World!")
}, },
} }
apiversion := &cobra.Command{ apiversion := &cobra.Command{
Use: "apiversion", Use: "apiversion",
Short: "Print the API version of the server", Short: "Print the API version of the server",
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
apiClient := dockerCLI.Client() cli := dockerCli.Client()
ping, err := apiClient.Ping(context.Background(), client.PingOptions{}) ping, err := cli.Ping(context.Background())
if err != nil { if err != nil {
return err return err
} }
_, _ = fmt.Println(ping.APIVersion) fmt.Println(ping.APIVersion)
return nil return nil
}, },
} }
@@ -39,15 +38,15 @@ func main() {
Use: "exitstatus2", Use: "exitstatus2",
Short: "Exit with status 2", Short: "Exit with status 2",
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Exiting with error status 2") fmt.Fprintln(dockerCli.Err(), "Exiting with error status 2")
os.Exit(2) os.Exit(2)
return nil return nil
}, },
} }
var ( var (
who, optContext string who, context string
preRun, debug bool preRun, debug bool
) )
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "helloworld", Use: "helloworld",
@@ -57,33 +56,33 @@ func main() {
return err return err
} }
if preRun { if preRun {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Plugin PersistentPreRunE called") fmt.Fprintf(dockerCli.Err(), "Plugin PersistentPreRunE called")
} }
return nil return nil
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if debug { if debug {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Plugin debug mode enabled") fmt.Fprintf(dockerCli.Err(), "Plugin debug mode enabled")
} }
switch optContext { switch context {
case "Christmas": case "Christmas":
_, _ = fmt.Fprintln(dockerCLI.Out(), "Merry Christmas!") fmt.Fprintf(dockerCli.Out(), "Merry Christmas!\n")
return nil return nil
case "": case "":
// nothing // nothing
} }
if who == "" { if who == "" {
who, _ = dockerCLI.ConfigFile().PluginConfig("helloworld", "who") who, _ = dockerCli.ConfigFile().PluginConfig("helloworld", "who")
} }
if who == "" { if who == "" {
who = "World" who = "World"
} }
_, _ = fmt.Fprintln(dockerCLI.Out(), "Hello", who) fmt.Fprintf(dockerCli.Out(), "Hello %s!\n", who)
dockerCLI.ConfigFile().SetPluginConfig("helloworld", "lastwho", who) dockerCli.ConfigFile().SetPluginConfig("helloworld", "lastwho", who)
return dockerCLI.ConfigFile().Save() return dockerCli.ConfigFile().Save()
}, },
} }
@@ -93,14 +92,15 @@ func main() {
// These are intended to deliberately clash with the CLIs own top // These are intended to deliberately clash with the CLIs own top
// level arguments. // level arguments.
flags.BoolVarP(&debug, "debug", "D", false, "Enable debug") 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) cmd.AddCommand(goodbye, apiversion, exitStatus2)
return cmd return cmd
}, },
metadata.Metadata{ manager.Metadata{
SchemaVersion: "0.1.0", SchemaVersion: "0.1.0",
Vendor: "Docker Inc.", Vendor: "Docker Inc.",
Version: "testing", Version: "testing",
Experimental: os.Getenv("HELLO_EXPERIMENTAL") != "",
}) })
} }
-85
View File
@@ -1,85 +0,0 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
// Package hooks defines the contract between the Docker CLI and CLI plugin hook
// implementations.
//
// # Audience
//
// This package is intended to be imported by CLI plugin implementations that
// implement a "hooks" subcommand, and by the Docker CLI when invoking those
// hooks.
//
// # Contract and wire format
//
// Hook inputs (see [Request]) are serialized as JSON and passed to the plugin hook
// subcommand (currently as a command-line argument). Hook outputs are emitted by
// the plugin as JSON (see [Response]).
//
// # Stability
//
// The types that represent the hook contract ([Request], [Response] and related
// constants) are considered part of Docker CLI's public Go API.
// Fields and values may be extended in a backwards-compatible way (for example,
// adding new fields), but existing fields and their meaning should remain stable.
// Plugins should ignore unknown fields and unknown hook types to remain
// forwards-compatible.
package hooks
// ResponseType is the type of response from the plugin.
type ResponseType int
const (
NextSteps ResponseType = 0
)
// Request is the type representing the information
// that plugins declaring support for hooks get passed when
// being invoked following a CLI command execution.
type Request 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
// is invoked with "context".
RootCmd string `json:"RootCmd,omitzero"`
// Flags contains flags that were set on the command for which the
// hook was invoked. It uses flag names as key, with leading hyphens
// removed ("--flag" and "-flag" are included as "flag" and "f").
//
// Flag values are not included and are set to an empty string,
// except for boolean flags known to the CLI itself, for which
// the value is either "true", or "false".
//
// Plugins can use this information to adjust their [Response]
// based on whether the command triggering the hook was invoked
// with.
Flags map[string]string `json:"Flags,omitzero"`
// CommandError is a string containing the error output (if any)
// of the command for which the hook was invoked.
CommandError string `json:"CommandError,omitzero"`
}
// Response 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 Response struct {
Type ResponseType `json:"Type"`
Template string `json:"Template,omitzero"`
}
// HookType is the type of response from the plugin.
//
// Deprecated: use [ResponseType] instead.
//
//go:fix inline
type HookType = ResponseType
// HookMessage represents a plugin hook response.
//
// Deprecated: use [Response] instead.
//
//go:fix inline
type HookMessage = Response
-75
View File
@@ -1,75 +0,0 @@
package hooks
import (
"fmt"
)
const (
hookTemplateCommandName = `{{command}}`
hookTemplateFlagValue = `{{flagValue %q}}`
hookTemplateArg = `{{argValue %d}}`
)
// TemplateReplaceSubcommandName returns a hook template string
// that will be replaced by the CLI subcommand being executed
//
// Example:
//
// Response{
// Type: NextSteps,
// Template: "you ran the subcommand: " + TemplateReplaceSubcommandName(),
// }
//
// When being executed after the command:
//
// docker run --name "my-container" alpine
//
// It results in the message:
//
// you ran the subcommand: run
func TemplateReplaceSubcommandName() string {
return hookTemplateCommandName
}
// TemplateReplaceFlagValue returns a hook template string that will be
// replaced with the flags value when printed by the CLI.
//
// Example:
//
// Response{
// Type: NextSteps,
// Template: "you ran a container named: " + TemplateReplaceFlagValue("name"),
// }
//
// when executed after the command:
//
// docker run --name "my-container" alpine
//
// it results 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:
//
// Response{
// Type: NextSteps,
// Template: "run this image with `docker run " + TemplateReplaceArg(0) + "`",
// }
//
// when being executed after the command:
//
// docker pull alpine
//
// It results in the message:
//
// Run this image with `docker run alpine`
func TemplateReplaceArg(i int) string {
return fmt.Sprintf(hookTemplateArg, i)
}
-50
View File
@@ -1,50 +0,0 @@
package hooks_test
import (
"testing"
"github.com/docker/cli/cli-plugins/hooks"
)
func TestTemplateHelpers(t *testing.T) {
tests := []struct {
doc string
got func() string
want string
}{
{
doc: "subcommand name",
got: hooks.TemplateReplaceSubcommandName,
want: `{{command}}`,
},
{
doc: "flag value",
got: func() string {
return hooks.TemplateReplaceFlagValue("name")
},
want: `{{flagValue "name"}}`,
},
{
doc: "arg",
got: func() string {
return hooks.TemplateReplaceArg(0)
},
want: `{{argValue 0}}`,
},
{
doc: "arg",
got: func() string {
return hooks.TemplateReplaceArg(3)
},
want: `{{argValue 3}}`,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
if got := tc.got(); got != tc.want {
t.Fatalf("expected %q, got %q", tc.want, got)
}
})
}
}
-23
View File
@@ -1,23 +0,0 @@
package hooks
import "io"
const (
whatsNext = "\n\033[1mWhat's next:\033[0m\n"
indent = " "
)
// PrintNextSteps renders list of [NextSteps] messages and writes them
// to out. It is a no-op if messages is empty.
func PrintNextSteps(out io.Writer, messages []string) {
if len(messages) == 0 {
return
}
_, _ = io.WriteString(out, whatsNext)
for _, msg := range messages {
_, _ = io.WriteString(out, indent)
_, _ = io.WriteString(out, msg)
_, _ = io.WriteString(out, "\n")
}
}
-45
View File
@@ -1,45 +0,0 @@
package hooks_test
import (
"strings"
"testing"
"github.com/docker/cli/cli-plugins/hooks"
"gotest.tools/v3/assert"
)
func TestPrintHookMessages(t *testing.T) {
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
tests := []struct {
doc string
messages []string
expectedOutput string
}{
{
doc: "no messages",
messages: nil,
expectedOutput: "",
},
{
doc: "single message",
messages: []string{"Bork!"},
expectedOutput: header +
" Bork!\n",
},
{
doc: "multiple messages",
messages: []string{"Foo", "bar"},
expectedOutput: header +
" Foo\n" +
" bar\n",
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
var w strings.Builder
hooks.PrintNextSteps(&w, tc.messages)
assert.Equal(t, w.String(), tc.expectedOutput)
})
}
}
-95
View File
@@ -1,95 +0,0 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package hooks
import (
"bytes"
"errors"
"fmt"
"strings"
"text/template"
"github.com/spf13/cobra"
)
const maxMessages = 10
func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
out := hookTemplate
if strings.Contains(hookTemplate, "{{") {
// Message may be a template.
msgContext := commandInfo{cmd: cmd}
tmpl, err := template.New("").Funcs(template.FuncMap{
"command": msgContext.command,
"flagValue": msgContext.flagValue,
"argValue": msgContext.argValue,
// kept for backward-compatibility with old templates.
"flag": func(_ any, flagName string) (string, error) { return msgContext.flagValue(flagName) },
"arg": func(_ any, i int) (string, error) { return msgContext.argValue(i) },
}).Parse(hookTemplate)
if err != nil {
return nil, err
}
var b bytes.Buffer
err = tmpl.Execute(&b, msgContext)
if err != nil {
return nil, err
}
out = b.String()
}
if n := strings.Count(out, "\n"); n > maxMessages {
return nil, fmt.Errorf("hook template contains too many messages (%d): maximum is %d", n, maxMessages)
}
return strings.SplitN(out, "\n", maxMessages), nil
}
var ErrHookTemplateParse = errors.New("failed to parse hook template")
// commandInfo provides info about the command for which the hook was invoked.
// It is used for templated hook-messages.
type commandInfo struct {
cmd *cobra.Command
}
// Name returns the name of the (sub)command for which the hook was invoked.
//
// It's used for backward-compatibility with old templates.
func (c commandInfo) Name() string {
return c.command()
}
// command returns the name of the (sub)command for which the hook was invoked.
func (c commandInfo) command() string {
if c.cmd == nil {
return ""
}
return c.cmd.Name()
}
// flagValue returns the value that was set for the given flag when the hook was invoked.
func (c commandInfo) flagValue(flagName string) (string, error) {
if c.cmd == nil {
return "", fmt.Errorf("%w: flagValue: cmd is nil", ErrHookTemplateParse)
}
f := c.cmd.Flag(flagName)
if f == nil {
return "", fmt.Errorf("%w: flagValue: no flags found", ErrHookTemplateParse)
}
return f.Value.String(), nil
}
// argValue returns the value of the nth argument.
func (c commandInfo) argValue(n int) (string, error) {
if c.cmd == nil {
return "", fmt.Errorf("%w: arg: cmd is nil", ErrHookTemplateParse)
}
flags := c.cmd.Flags()
v := flags.Arg(n)
if v == "" && n >= flags.NArg() {
return "", fmt.Errorf("%w: arg: %dth argument not set", ErrHookTemplateParse, n)
}
return v, nil
}
-125
View File
@@ -1,125 +0,0 @@
package hooks_test
import (
"testing"
"github.com/docker/cli/cli-plugins/hooks"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
)
// TestParseTemplate tests parsing templates as returned by plugins.
//
// It uses fixed string fixtures to lock in compatibility with existing
// plugin templates, so older formats continue to work even if we add new
// template forms.
//
// For helper-backed cases, it also verifies that templates produced by the
// current TemplateReplace* helpers parse to the same output. This lets us
// evolve the emitted template format without breaking older plugins.
func TestParseTemplate(t *testing.T) {
type testFlag struct {
name string
value string
}
tests := []struct {
doc string
template string // compatibility fixture; keep even if helpers emit a newer form
templateFunc func() string
flags []testFlag
args []string
expectedOutput []string
}{
{
doc: "empty template",
template: "",
expectedOutput: []string{""},
},
{
doc: "plain message",
template: "a plain template message",
expectedOutput: []string{"a plain template message"},
},
{
doc: "subcommand name",
template: "hello {{.Name}}", // NOTE: fixture; do not modify without considering plugin compatibility
templateFunc: func() string { return "hello " + hooks.TemplateReplaceSubcommandName() },
expectedOutput: []string{"hello pull"},
},
{
doc: "single flag",
template: `{{flag . "tag"}}`, // NOTE: fixture; do not modify without considering plugin compatibility
templateFunc: func() string { return hooks.TemplateReplaceFlagValue("tag") },
flags: []testFlag{
{name: "tag", value: "my-tag"},
},
expectedOutput: []string{"my-tag"},
},
{
doc: "multiple flags",
template: `{{flag . "test-one"}} {{flag . "test2"}}`, // NOTE: fixture; do not modify without considering plugin compatibility
templateFunc: func() string {
return hooks.TemplateReplaceFlagValue("test-one") + " " + hooks.TemplateReplaceFlagValue("test2")
},
flags: []testFlag{
{
name: "test-one",
value: "value",
},
{
name: "test2",
value: "value2",
},
},
expectedOutput: []string{"value value2"},
},
{
doc: "multiple args",
template: `{{arg . 0}} {{arg . 1}}`, // NOTE: fixture; do not modify without considering plugin compatibility
templateFunc: func() string { return hooks.TemplateReplaceArg(0) + " " + hooks.TemplateReplaceArg(1) },
args: []string{"zero", "one"},
expectedOutput: []string{"zero one"},
},
{
doc: "arg in sentence",
template: "You just pulled {{arg . 0}}", // NOTE: fixture; do not modify without considering plugin compatibility
templateFunc: func() string { return "You just pulled " + hooks.TemplateReplaceArg(0) },
args: []string{"alpine"},
expectedOutput: []string{"You just pulled alpine"},
},
{
doc: "multiline output",
template: "one line\nanother line!",
expectedOutput: []string{"one line", "another line!"},
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
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)
// Validate using fixtures.
out, err := hooks.ParseTemplate(tc.template, testCmd)
assert.NilError(t, err)
assert.DeepEqual(t, out, tc.expectedOutput)
if tc.templateFunc != nil {
// Validate using the current template function equivalent.
out, err = hooks.ParseTemplate(tc.templateFunc(), testCmd)
assert.NilError(t, err)
assert.DeepEqual(t, out, tc.expectedOutput)
}
})
}
}
+7 -3
View File
@@ -2,10 +2,14 @@ package manager
import ( import (
"os/exec" "os/exec"
"github.com/docker/cli/cli-plugins/metadata"
) )
// Candidate represents a possible plugin candidate, for mocking purposes
type Candidate interface {
Path() string
Metadata() ([]byte, error)
}
type candidate struct { type candidate struct {
path string path string
} }
@@ -15,5 +19,5 @@ func (c *candidate) Path() string {
} }
func (c *candidate) Metadata() ([]byte, error) { func (c *candidate) Metadata() ([]byte, error) {
return exec.Command(c.path, metadata.MetadataSubcommandName).Output() // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments" return exec.Command(c.path, MetadataSubcommandName).Output()
} }
+43 -112
View File
@@ -6,16 +6,16 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gotest.tools/v3/assert" "gotest.tools/assert"
is "gotest.tools/v3/assert/cmp" "gotest.tools/assert/cmp"
) )
type fakeCandidate struct { type fakeCandidate struct {
path string path string
exec bool exec bool
meta string meta string
allowExperimental bool
} }
func (c *fakeCandidate) Path() string { func (c *fakeCandidate) Path() string {
@@ -30,133 +30,64 @@ func (c *fakeCandidate) Metadata() ([]byte, error) {
} }
func TestValidateCandidate(t *testing.T) { func TestValidateCandidate(t *testing.T) {
const ( var (
goodPluginName = metadata.NamePrefix + "goodplugin" goodPluginName = NamePrefix + "goodplugin"
builtinName = metadata.NamePrefix + "builtin"
builtinAlias = metadata.NamePrefix + "alias"
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble" builtinName = NamePrefix + "builtin"
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456" builtinAlias = NamePrefix + "alias"
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
metaExperimental = `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`
) )
fakeroot := &cobra.Command{Use: "docker"} fakeroot := &cobra.Command{Use: "docker"}
fakeroot.AddCommand(&cobra.Command{ fakeroot.AddCommand(&cobra.Command{
Use: strings.TrimPrefix(builtinName, metadata.NamePrefix), Use: strings.TrimPrefix(builtinName, NamePrefix),
Aliases: []string{ Aliases: []string{
strings.TrimPrefix(builtinAlias, metadata.NamePrefix), strings.TrimPrefix(builtinAlias, NamePrefix),
}, },
}) })
for _, tc := range []struct { for _, tc := range []struct {
name string name string
plugin *fakeCandidate c *fakeCandidate
// Either err or invalid may be non-empty, but not both (both can be empty for a good plugin). // Either err or invalid may be non-empty, but not both (both can be empty for a good plugin).
err string err string
invalid string invalid string
expVer string
}{ }{
// Invalid cases. /* Each failing one of the tests */
{ {name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
name: "empty path", {name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", NamePrefix)},
plugin: &fakeCandidate{path: ""}, {name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
err: "plugin candidate path cannot be empty", {name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`},
}, {name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`},
{ {name: "fetch failure", c: &fakeCandidate{path: goodPluginPath, exec: false}, invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath)},
name: "bad prefix", {name: "metadata not json", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`}, invalid: "invalid character"},
plugin: &fakeCandidate{path: badPrefixPath}, {name: "empty schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`}, invalid: `plugin SchemaVersion "" is not valid`},
err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix), {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: "bad path", {name: "experimental required", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental}, invalid: "requires experimental CLI"},
plugin: &fakeCandidate{path: badNamePath}, // This one should work
invalid: "did not match", {name: "valid", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}},
}, {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}},
name: "builtin command",
plugin: &fakeCandidate{path: builtinName},
invalid: `plugin "builtin" duplicates builtin command`,
},
{
name: "builtin alias",
plugin: &fakeCandidate{path: builtinAlias},
invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`,
},
{
name: "fetch failure",
plugin: &fakeCandidate{path: goodPluginPath, exec: false},
invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath),
},
{
name: "metadata not json",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`},
invalid: "invalid character",
},
{
name: "empty schemaversion",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`},
invalid: `plugin SchemaVersion version cannot be empty`,
},
{
name: "invalid schemaversion",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`},
invalid: `plugin SchemaVersion "xyzzy" has wrong format: must be <major>.<minor>.<patch>`,
},
{
name: "invalid schemaversion major",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "2.0.0"}`},
invalid: `plugin SchemaVersion "2.0.0" is not supported: must be lower than 2.0.0`,
},
{
name: "no vendor",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`},
invalid: "plugin metadata does not define a vendor",
},
{
name: "empty vendor",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`},
invalid: "plugin metadata does not define a vendor",
},
// Valid cases.
{
name: "valid",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`},
expVer: "0.1.0",
},
{
// Including the deprecated "experimental" field should not break processing.
name: "with legacy experimental",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`},
expVer: "0.1.0",
},
{
// note that this may not be supported by older CLIs
name: "new minor schema version",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.2.0", "Vendor": "e2e-testing"}`},
expVer: "0.2.0",
},
{
// note that this may not be supported by older CLIs
name: "new major schema version",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "1.0.0", "Vendor": "e2e-testing"}`},
expVer: "1.0.0",
},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
p, err := newPlugin(tc.plugin, fakeroot.Commands()) p, err := newPlugin(tc.c, fakeroot, tc.c.allowExperimental)
switch { if tc.err != "" {
case tc.err != "":
assert.ErrorContains(t, err, tc.err) assert.ErrorContains(t, err, tc.err)
case tc.invalid != "": } else if tc.invalid != "" {
assert.NilError(t, err) assert.NilError(t, err)
assert.Assert(t, is.ErrorType(p.Err, reflect.TypeFor[*pluginError]())) assert.Assert(t, cmp.ErrorType(p.Err, reflect.TypeOf(&pluginError{})))
assert.ErrorContains(t, p.Err, tc.invalid) assert.ErrorContains(t, p.Err, tc.invalid)
default: } else {
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, metadata.NamePrefix+p.Name, goodPluginName) assert.Equal(t, NamePrefix+p.Name, goodPluginName)
assert.Equal(t, p.SchemaVersion, tc.expVer) assert.Equal(t, p.SchemaVersion, "0.1.0")
assert.Equal(t, p.Vendor, "e2e-testing") assert.Equal(t, p.Vendor, "e2e-testing")
} }
}) })
+47 -68
View File
@@ -1,81 +1,60 @@
package manager package manager
import ( import (
"fmt" "github.com/docker/cli/cli/command"
"os"
"sync"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/config"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var pluginCommandStubsOnce sync.Once const (
// CommandAnnotationPlugin is added to every stub command added by
// AddPluginCommandStubs with the value "true" and so can be
// used to distinguish plugin stubs from regular commands.
CommandAnnotationPlugin = "com.docker.cli.plugin"
// CommandAnnotationPluginVendor is added to every stub command
// added by AddPluginCommandStubs and contains the vendor of
// that plugin.
CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor"
// CommandAnnotationPluginVersion is added to every stub command
// added by AddPluginCommandStubs and contains the version of
// that plugin.
CommandAnnotationPluginVersion = "com.docker.cli.plugin.version"
// CommandAnnotationPluginInvalid is added to any stub command
// added by AddPluginCommandStubs for an invalid command (that
// is, one which failed it's candidate test) and contains the
// reason for the failure.
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
)
// AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid // AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid
// plugin. The command stubs will have several annotations added, see // plugin. The command stubs will have several annotations added, see
// `CommandAnnotationPlugin*`. // `CommandAnnotationPlugin*`.
func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (err error) { func AddPluginCommandStubs(dockerCli command.Cli, cmd *cobra.Command) error {
pluginCommandStubsOnce.Do(func() { plugins, err := ListPlugins(dockerCli, cmd)
var plugins []Plugin if err != nil {
plugins, err = ListPlugins(dockerCLI, rootCmd) return err
if err != nil { }
return for _, p := range plugins {
vendor := p.Vendor
if vendor == "" {
vendor = "unknown"
} }
for _, p := range plugins { annotations := map[string]string{
vendor := p.Vendor CommandAnnotationPlugin: "true",
if vendor == "" { CommandAnnotationPluginVendor: vendor,
vendor = "unknown" CommandAnnotationPluginVersion: p.Version,
}
annotations := map[string]string{
metadata.CommandAnnotationPlugin: "true",
metadata.CommandAnnotationPluginVendor: vendor,
metadata.CommandAnnotationPluginVersion: p.Version,
}
if p.Err != nil {
annotations[metadata.CommandAnnotationPluginInvalid] = p.Err.Error()
}
rootCmd.AddCommand(&cobra.Command{
Use: p.Name,
Short: p.ShortDescription,
Hidden: p.Hidden,
Run: func(_ *cobra.Command, _ []string) {},
Annotations: annotations,
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
flags := rootCmd.PersistentFlags()
flags.SetOutput(nil)
if err := flags.Parse(args); err != nil {
return err
}
if flags.Changed("help") {
cmd.HelpFunc()(rootCmd, args)
return nil
}
return fmt.Errorf("docker: unknown command: docker %s\n\nRun 'docker --help' for more information", 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} //nolint:prealloc // no need to over-complicate things.
cargs = append(cargs, args...)
cargs = append(cargs, toComplete)
origArgs := os.Args
os.Args = cargs
defer func() {
os.Args = origArgs
}()
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
},
})
} }
}) if p.Err != nil {
return err annotations[CommandAnnotationPluginInvalid] = p.Err.Error()
}
cmd.AddCommand(&cobra.Command{
Use: p.Name,
Short: p.ShortDescription,
Run: func(_ *cobra.Command, _ []string) {},
Annotations: annotations,
})
}
return nil
} }
-86
View File
@@ -1,86 +0,0 @@
package manager
import (
"os"
"path/filepath"
"slices"
"sync"
"testing"
"github.com/docker/cli/internal/test"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
)
func TestPluginResourceAttributesEnvvar(t *testing.T) {
cmd := &cobra.Command{
Annotations: map[string]string{
cobra.CommandDisplayNameAnnotation: "docker",
},
}
// Ensure basic usage is fine.
env := appendPluginResourceAttributesEnvvar(nil, cmd, Plugin{Name: "compose"})
assert.DeepEqual(t, []string{"OTEL_RESOURCE_ATTRIBUTES=docker.cli.cobra.command_path=docker%20compose"}, env)
// Add a user-based environment variable to OTEL_RESOURCE_ATTRIBUTES.
t.Setenv("OTEL_RESOURCE_ATTRIBUTES", "a.b.c=foo")
env = appendPluginResourceAttributesEnvvar(nil, cmd, Plugin{Name: "compose"})
assert.DeepEqual(t, []string{"OTEL_RESOURCE_ATTRIBUTES=a.b.c=foo,docker.cli.cobra.command_path=docker%20compose"}, env)
}
func TestPluginStubRunEReturnsParseError(t *testing.T) {
cmd, err := preparePluginStubCommand(t)
assert.NilError(t, err)
err = cmd.RunE(cmd, []string{"--definitely-not-a-real-flag"})
assert.ErrorContains(t, err, "unknown flag: --definitely-not-a-real-flag")
}
func TestPluginStubCompletionRestoresOSArgs(t *testing.T) {
cmd, err := preparePluginStubCommand(t)
assert.NilError(t, err)
savedArgs := os.Args
t.Cleanup(func() { os.Args = savedArgs })
originalArgs := []string{"docker", "image", "ls"}
os.Args = slices.Clone(originalArgs)
_, directive := cmd.ValidArgsFunction(cmd, []string{"--all"}, "alp")
assert.Equal(t, directive, cobra.ShellCompDirectiveError)
assert.DeepEqual(t, os.Args, originalArgs)
}
func preparePluginStubCommand(t *testing.T) (*cobra.Command, error) {
t.Helper()
pluginCommandStubsOnce = sync.Once{}
tmpDir := t.TempDir()
const cliPlugin = `#!/bin/sh
printf '%s' '{"SchemaVersion":"0.1.0"}'
`
if err := os.WriteFile(filepath.Join(tmpDir, "docker-testplugin"), []byte(cliPlugin), 0o777); err != nil {
return nil, err
}
cli := test.NewFakeCli(nil)
cli.ConfigFile().CLIPluginsExtraDirs = []string{tmpDir}
root := &cobra.Command{Use: "docker"}
root.PersistentFlags().Bool("debug", false, "")
if err := AddPluginCommandStubs(cli, root); err != nil {
return nil, err
}
cmd, _, err := root.Find([]string{"testplugin"})
if err != nil {
return nil, err
}
if cmd == nil {
return nil, os.ErrNotExist
}
return cmd, nil
}
+9 -12
View File
@@ -1,10 +1,7 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package manager package manager
import ( import (
"fmt" "github.com/pkg/errors"
) )
// pluginError is set as Plugin.Err by NewPlugin if the plugin // pluginError is set as Plugin.Err by NewPlugin if the plugin
@@ -23,24 +20,24 @@ func (e *pluginError) Error() string {
return e.cause.Error() return e.cause.Error()
} }
// Unwrap provides compatibility for Go 1.13 error chains. // Cause satisfies the errors.causer interface for pluginError.
func (e *pluginError) Unwrap() error { func (e *pluginError) Cause() error {
return e.cause return e.cause
} }
// MarshalText marshals the pluginError into a textual form. // MarshalText marshalls the pluginError into a textual form.
func (e *pluginError) MarshalText() (text []byte, err error) { func (e *pluginError) MarshalText() (text []byte, err error) {
return []byte(e.cause.Error()), nil return []byte(e.cause.Error()), nil
} }
// wrapAsPluginError wraps an error in a pluginError with an // wrapAsPluginError wraps an error in a pluginError with an
// additional message. // additional message, analogous to errors.Wrapf.
func wrapAsPluginError(err error, msg string) error { func wrapAsPluginError(err error, msg string) error {
return &pluginError{cause: fmt.Errorf("%s: %w", msg, err)} return &pluginError{cause: errors.Wrap(err, msg)}
} }
// newPluginError creates a new pluginError, analogous to // NewPluginError creates a new pluginError, analogous to
// errors.Errorf. // errors.Errorf.
func newPluginError(msg string, args ...any) error { func NewPluginError(msg string, args ...interface{}) error {
return &pluginError{cause: fmt.Errorf(msg, args...)} return &pluginError{cause: errors.Errorf(msg, args...)}
} }
+12 -15
View File
@@ -1,27 +1,24 @@
package manager package manager
import ( import (
"encoding/json" "fmt"
"errors"
"testing" "testing"
"gotest.tools/v3/assert" "github.com/pkg/errors"
is "gotest.tools/v3/assert/cmp" "gopkg.in/yaml.v2"
"gotest.tools/assert"
) )
func TestPluginError(t *testing.T) { func TestPluginError(t *testing.T) {
err := newPluginError("new error") 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") err = wrapAsPluginError(inner, "wrapping")
assert.Check(t, is.Error(err, "wrapping: testing")) assert.Error(t, err, "wrapping: testing")
assert.Check(t, is.ErrorIs(err, inner)) assert.Equal(t, inner, errors.Cause(err))
actual, err := json.Marshal(err) actual, err := yaml.Marshal(err)
assert.Check(t, err) assert.NilError(t, err)
assert.Check(t, is.Equal(`"wrapping: testing"`, string(actual))) assert.Equal(t, "'wrapping: testing'\n", string(actual))
err = wrapAsPluginError(nil, "wrapping")
assert.Check(t, is.Error(err, "wrapping: %!w(<nil>)"))
} }
-237
View File
@@ -1,237 +0,0 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package manager
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"github.com/docker/cli/cli-plugins/hooks"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"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.
//
// Deprecated: use [hooks.Request] instead.
//
//go:fix inline
type HookPluginData = hooks.Request
// 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 config.Provider, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
flags := getCommandFlags(subCommand)
runHooks(ctx, dockerCLI.ConfigFile(), 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 config.Provider, rootCmd, subCommand *cobra.Command, args []string, cmdErrorMessage string) {
commandName := strings.Join(args, " ")
flags := getNaiveFlags(args)
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, cmdErrorMessage)
}
func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
nextSteps := invokeAndCollectHooks(ctx, cfg, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
hooks.PrintNextSteps(subCommand.ErrOrStderr(), nextSteps)
}
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
if ctx.Err() != nil {
return nil
}
pluginsCfg := cfg.Plugins
if pluginsCfg == nil {
return nil
}
pluginDirs := getPluginDirs(cfg)
nextSteps := make([]string, 0, len(pluginsCfg))
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (messages []string, ok bool, err error) {
match, matched := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage)
if !matched {
return nil, false, nil
}
p, err := getPlugin(pluginName, pluginDirs, rootCmd)
if err != nil {
return nil, false, err
}
resp, err := p.RunHook(ctx, hooks.Request{
RootCmd: match,
Flags: flags,
CommandError: cmdErrorMessage,
})
if err != nil {
return nil, false, err
}
var message hooks.Response
if err := json.Unmarshal(resp, &message); err != nil {
return nil, false, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
}
// currently the only hook type
if message.Type != hooks.NextSteps {
return nil, false, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
}
messages, err = hooks.ParseTemplate(message.Template, subCmd)
if err != nil {
return nil, false, err
}
return messages, true, nil
}
for pluginName, pluginCfg := range pluginsCfg {
messages, ok, err := tryInvokeHook(pluginName, pluginCfg)
if err != nil {
// skip misbehaving plugins, but don't halt execution
logrus.WithFields(logrus.Fields{
"error": err,
"plugin": pluginName,
}).Debug("Plugin hook invocation failed")
continue
}
if !ok {
continue
}
var appended bool
nextSteps, appended = appendNextSteps(nextSteps, messages)
if !appended {
logrus.WithFields(logrus.Fields{
"plugin": pluginName,
}).Debug("Plugin responded with an empty hook message; ignoring")
}
}
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.
//
// Plugins can declare two types of hooks in their configuration:
// - "hooks": fires on every command invocation (success or failure)
// - "error-hooks": fires only when a command fails (cmdErrorMessage is non-empty)
func pluginMatch(pluginCfg map[string]string, subCmd string, cmdErrorMessage string) (string, bool) {
// Check "hooks" first — these always fire regardless of command outcome.
if match, ok := matchHookConfig(pluginCfg["hooks"], subCmd); ok {
return match, true
}
// Check "error-hooks" — these only fire when there was an error.
if cmdErrorMessage != "" {
if match, ok := matchHookConfig(pluginCfg["error-hooks"], subCmd); ok {
return match, true
}
}
return "", false
}
// matchHookConfig checks if a comma-separated hook configuration string
// contains a prefix match for the given subcommand.
func matchHookConfig(configuredHooks string, subCmd string) (string, bool) {
if configuredHooks == "" {
return "", false
}
for hookCmd := range strings.SplitSeq(configuredHooks, ",") {
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
}
-375
View File
@@ -1,375 +0,0 @@
package manager
import (
"context"
"testing"
"github.com/docker/cli/cli/config/configfile"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
type fakeConfigProvider struct {
cfg *configfile.ConfigFile
}
func (f *fakeConfigProvider) ConfigFile() *configfile.ConfigFile {
return f.cfg
}
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 {
doc string
commandString string
pluginConfig map[string]string
cmdErrorMessage string
expectedMatch string
expectedOk bool
}{
{
doc: "hooks prefix match",
commandString: "image ls",
pluginConfig: map[string]string{
"hooks": "image",
},
expectedMatch: "image",
expectedOk: true,
},
{
doc: "hooks no match",
commandString: "context ls",
pluginConfig: map[string]string{
"hooks": "build",
},
expectedMatch: "",
expectedOk: false,
},
{
doc: "hooks exact match",
commandString: "context ls",
pluginConfig: map[string]string{
"hooks": "context ls",
},
expectedMatch: "context ls",
expectedOk: true,
},
{
doc: "hooks first match wins",
commandString: "image ls",
pluginConfig: map[string]string{
"hooks": "image ls,image",
},
expectedMatch: "image ls",
expectedOk: true,
},
{
doc: "hooks empty string",
commandString: "image ls",
pluginConfig: map[string]string{
"hooks": "",
},
expectedMatch: "",
expectedOk: false,
},
{
doc: "hooks partial token no match",
commandString: "image inspect",
pluginConfig: map[string]string{
"hooks": "image i",
},
expectedMatch: "",
expectedOk: false,
},
{
doc: "hooks prefix token match",
commandString: "image inspect",
pluginConfig: map[string]string{
"hooks": "image",
},
expectedMatch: "image",
expectedOk: true,
},
{
doc: "error-hooks match on error",
commandString: "build",
pluginConfig: map[string]string{
"error-hooks": "build",
},
cmdErrorMessage: "exit status 1",
expectedMatch: "build",
expectedOk: true,
},
{
doc: "error-hooks no match on success",
commandString: "build",
pluginConfig: map[string]string{
"error-hooks": "build",
},
cmdErrorMessage: "",
expectedMatch: "",
expectedOk: false,
},
{
doc: "error-hooks prefix match on error",
commandString: "compose up",
pluginConfig: map[string]string{
"error-hooks": "compose",
},
cmdErrorMessage: "exit status 1",
expectedMatch: "compose",
expectedOk: true,
},
{
doc: "error-hooks no match for wrong command",
commandString: "pull",
pluginConfig: map[string]string{
"error-hooks": "build",
},
cmdErrorMessage: "exit status 1",
expectedMatch: "",
expectedOk: false,
},
{
doc: "hooks takes precedence over error-hooks",
commandString: "build",
pluginConfig: map[string]string{
"hooks": "build",
"error-hooks": "build",
},
cmdErrorMessage: "exit status 1",
expectedMatch: "build",
expectedOk: true,
},
{
doc: "hooks fires on success even with error-hooks configured",
commandString: "build",
pluginConfig: map[string]string{
"hooks": "build",
"error-hooks": "build",
},
cmdErrorMessage: "",
expectedMatch: "build",
expectedOk: true,
},
{
doc: "error-hooks with multiple commands",
commandString: "compose up",
pluginConfig: map[string]string{
"error-hooks": "build,compose up,pull",
},
cmdErrorMessage: "exit status 1",
expectedMatch: "compose up",
expectedOk: true,
},
}
for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
match, ok := pluginMatch(tc.pluginConfig, tc.commandString, tc.cmdErrorMessage)
assert.Equal(t, ok, tc.expectedOk)
assert.Equal(t, match, tc.expectedMatch)
})
}
}
func TestMatchHookConfig(t *testing.T) {
testCases := []struct {
doc string
configuredHooks string
subCmd string
expectedMatch string
expectedOk bool
}{
{
doc: "empty config",
configuredHooks: "",
subCmd: "build",
expectedMatch: "",
expectedOk: false,
},
{
doc: "exact match",
configuredHooks: "build",
subCmd: "build",
expectedMatch: "build",
expectedOk: true,
},
{
doc: "prefix match",
configuredHooks: "image",
subCmd: "image ls",
expectedMatch: "image",
expectedOk: true,
},
{
doc: "comma-separated match",
configuredHooks: "pull,build,push",
subCmd: "build",
expectedMatch: "build",
expectedOk: true,
},
{
doc: "no match",
configuredHooks: "pull,push",
subCmd: "build",
expectedMatch: "",
expectedOk: false,
},
}
for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
match, ok := matchHookConfig(tc.configuredHooks, tc.subCmd)
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))
})
}
}
func TestRunPluginHooksPassesErrorMessage(t *testing.T) {
cfg := configfile.New("")
cfg.Plugins = map[string]map[string]string{
"test-plugin": {"hooks": "build"},
}
provider := &fakeConfigProvider{cfg: cfg}
root := &cobra.Command{Use: "docker"}
sub := &cobra.Command{Use: "build"}
root.AddCommand(sub)
// Should not panic with empty error message (success case)
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "")
// Should not panic with non-empty error message (failure case)
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "exit status 1")
}
func TestRunPluginHooksErrorHooks(t *testing.T) {
cfg := configfile.New("")
cfg.Plugins = map[string]map[string]string{
"test-plugin": {"error-hooks": "build"},
}
provider := &fakeConfigProvider{cfg: cfg}
root := &cobra.Command{Use: "docker"}
sub := &cobra.Command{Use: "build"}
root.AddCommand(sub)
// Should not panic — error-hooks with error message
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "exit status 1")
// Should not panic — error-hooks with no error (should be skipped)
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "")
}
func TestInvokeAndCollectHooksErrorHooksSkippedOnSuccess(t *testing.T) {
cfg := configfile.New("")
cfg.Plugins = map[string]map[string]string{
"nonexistent": {"error-hooks": "build"},
}
root := &cobra.Command{Use: "docker"}
sub := &cobra.Command{Use: "build"}
root.AddCommand(sub)
// On success, error-hooks should not match, so the plugin
// binary is never looked up and no results are returned.
result := invokeAndCollectHooks(
context.Background(), cfg, root, sub,
"build", map[string]string{}, "",
)
assert.Check(t, is.Len(result, 0))
}
func TestInvokeAndCollectHooksNoPlugins(t *testing.T) {
cfg := configfile.New("")
root := &cobra.Command{Use: "docker"}
sub := &cobra.Command{Use: "build"}
root.AddCommand(sub)
result := invokeAndCollectHooks(
context.Background(), cfg, root, sub,
"build", map[string]string{}, "some error",
)
assert.Check(t, is.Len(result, 0))
}
func TestInvokeAndCollectHooksCancelledContext(t *testing.T) {
cfg := configfile.New("")
cfg.Plugins = map[string]map[string]string{
"test-plugin": {"hooks": "build"},
}
root := &cobra.Command{Use: "docker"}
sub := &cobra.Command{Use: "build"}
root.AddCommand(sub)
ctx, cancel := context.WithCancel(context.Background())
cancel() // cancel immediately
result := invokeAndCollectHooks(
ctx, cfg, root, sub,
"build", map[string]string{}, "exit status 1",
)
assert.Check(t, is.Nil(result))
}
+112 -138
View File
@@ -1,194 +1,167 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package manager package manager
import ( import (
"context" "fmt"
"errors" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
slices "slices"
"strings" "strings"
"sync"
"github.com/containerd/errdefs" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/debug"
"github.com/fvbommel/sortorder"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
) )
// ReexecEnvvar is the name of an ennvar which is set to the command
// used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI.
const ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
// errPluginNotFound is the error returned when a plugin could not be found. // errPluginNotFound is the error returned when a plugin could not be found.
type errPluginNotFound string type errPluginNotFound string
func (errPluginNotFound) NotFound() {} func (e errPluginNotFound) NotFound() {}
func (e errPluginNotFound) Error() string { func (e errPluginNotFound) Error() string {
return "Error: No such CLI plugin: " + string(e) return "Error: No such CLI plugin: " + string(e)
} }
// getPluginDirs returns the platform-specific locations to search for plugins type errPluginRequireExperimental string
// 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 {
var pluginDirs []string
if cfg != nil { // Note: errPluginRequireExperimental implements notFound so that the plugin
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...) // is skipped when listing the plugins.
} func (e errPluginRequireExperimental) NotFound() {}
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
pluginDirs = append(pluginDirs, pluginDir) func (e errPluginRequireExperimental) Error() string {
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...) return fmt.Sprintf("plugin candidate %q: requires experimental CLI", string(e))
return pluginDirs
} }
func addPluginCandidatesFromDir(res map[string][]string, d string) { type notFound interface{ NotFound() }
dentries, err := os.ReadDir(d)
// Silently ignore any directories which we cannot list (e.g. due to // IsNotFound is true if the given error is due to a plugin not being found.
// permissions or anything else) or which is not a directory func IsNotFound(err error) bool {
if e, ok := err.(*pluginError); ok {
err = e.Cause()
}
_, ok := err.(notFound)
return ok
}
func getPluginDirs(dockerCli command.Cli) ([]string, error) {
var pluginDirs []string
if cfg := dockerCli.ConfigFile(); cfg != nil {
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
}
pluginDir, err := config.Path("cli-plugins")
if err != nil { if err != nil {
return return nil, err
}
pluginDirs = append(pluginDirs, pluginDir)
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
return pluginDirs, nil
}
func addPluginCandidatesFromDir(res map[string][]string, d string) error {
dentries, err := ioutil.ReadDir(d)
if err != nil {
return err
} }
for _, dentry := range dentries { for _, dentry := range dentries {
switch mode := dentry.Type() & os.ModeType; mode { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list switch dentry.Mode() & os.ModeType {
case os.ModeSymlink: case 0, os.ModeSymlink:
if !debug.IsEnabled() { // Regular file or symlink, keep going
// Skip broken symlinks unless debug is enabled. With debug
// enabled, this will print a warning in "docker info".
if _, err := os.Stat(filepath.Join(d, dentry.Name())); errors.Is(err, os.ErrNotExist) {
continue
}
}
case 0:
// Regular file, keep going
default: default:
// Something else, ignore. // Something else, ignore.
continue continue
} }
name := dentry.Name() name := dentry.Name()
if !strings.HasPrefix(name, metadata.NamePrefix) { if !strings.HasPrefix(name, NamePrefix) {
continue continue
} }
name = strings.TrimPrefix(name, metadata.NamePrefix) name = strings.TrimPrefix(name, NamePrefix)
var err error var err error
if name, err = trimExeSuffix(name); err != nil { if name, err = trimExeSuffix(name); err != nil {
continue continue
} }
res[name] = append(res[name], filepath.Join(d, dentry.Name())) res[name] = append(res[name], filepath.Join(d, dentry.Name()))
} }
return nil
} }
// listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority. // listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority.
func listPluginCandidates(dirs []string) map[string][]string { func listPluginCandidates(dirs []string) (map[string][]string, error) {
result := make(map[string][]string) result := make(map[string][]string)
for _, d := range dirs { for _, d := range dirs {
addPluginCandidatesFromDir(result, d) // Silently ignore any directories which we cannot
// Stat (e.g. due to permissions or anything else) or
// which is not a directory.
if fi, err := os.Stat(d); err != nil || !fi.IsDir() {
continue
}
if err := addPluginCandidatesFromDir(result, d); err != nil {
// Silently ignore paths which don't exist.
if os.IsNotExist(err) {
continue
}
return nil, err // Or return partial result?
}
} }
return result return result, nil
}
// GetPlugin returns a plugin on the system by its name
func GetPlugin(name string, dockerCLI config.Provider, rootcmd *cobra.Command) (*Plugin, error) {
pluginDirs := getPluginDirs(dockerCLI.ConfigFile())
return getPlugin(name, pluginDirs, rootcmd)
}
func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugin, error) {
candidates := listPluginCandidates(pluginDirs)
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 !errdefs.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 // ListPlugins produces a list of the plugins available on the system
func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, error) { func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
pluginDirs := getPluginDirs(dockerCli.ConfigFile()) pluginDirs, err := getPluginDirs(dockerCli)
candidates := listPluginCandidates(pluginDirs) if err != nil {
if len(candidates) == 0 {
return nil, nil
}
var plugins []Plugin
var mu sync.Mutex
ctx := rootcmd.Context()
if ctx == nil {
// Fallback, mostly for tests that pass a bare cobra.command
ctx = context.Background()
}
eg, _ := errgroup.WithContext(ctx)
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 !errdefs.IsNotFound(p.Err) {
p.ShadowedPaths = paths[1:]
mu.Lock()
defer mu.Unlock()
plugins = append(plugins, p)
}
return nil
})
}(paths)
}
if err := eg.Wait(); err != nil {
return nil, err return nil, err
} }
slices.SortFunc(plugins, func(a, b Plugin) int { candidates, err := listPluginCandidates(pluginDirs)
return sortorder.NaturalCompare(a.Name, b.Name) if err != nil {
}) return nil, err
}
var plugins []Plugin
for _, paths := range candidates {
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)
}
}
return plugins, nil return plugins, nil
} }
// PluginRunCommand returns an [os/exec.Cmd] which when [os/exec.Cmd.Run] will execute the named plugin. // PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conflicts. // The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
// The error returned satisfies the [errdefs.IsNotFound] predicate if no plugin was found or if the first candidate plugin was invalid somehow. // The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Command) (*exec.Cmd, error) { func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
// This uses the full original args, not the args which may // This uses the full original args, not the args which may
// have been provided by cobra to our caller. This is because // have been provided by cobra to our caller. This is because
// they lack e.g. global options which we must propagate here. // they lack e.g. global options which we must propagate here.
args := os.Args[1:] args := os.Args[1:]
if !isValidPluginName(name) { if !pluginNameRe.MatchString(name) {
// We treat this as "not found" so that callers will // We treat this as "not found" so that callers will
// fallback to their "invalid" command path. // fallback to their "invalid" command path.
return nil, errPluginNotFound(name) return nil, errPluginNotFound(name)
} }
exename := addExeSuffix(metadata.NamePrefix + name) exename := addExeSuffix(NamePrefix + name)
pluginDirs := getPluginDirs(dockerCli.ConfigFile()) pluginDirs, err := getPluginDirs(dockerCli)
if err != nil {
return nil, err
}
for _, d := range pluginDirs { for _, d := range pluginDirs {
path := filepath.Join(d, exename) path := filepath.Join(d, exename)
@@ -202,16 +175,22 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
} }
c := &candidate{path: path} c := &candidate{path: path}
plugin, err := newPlugin(c, rootcmd.Commands()) plugin, err := newPlugin(c, rootcmd, dockerCli.ClientInfo().HasExperimental)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if plugin.Err != nil { if plugin.Err != nil {
// TODO: why are we not returning plugin.Err? // 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) return nil, errPluginNotFound(name)
} }
cmd := exec.Command(plugin.Path, args...) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments" cmd := exec.Command(plugin.Path, args...)
// Using dockerCli.{In,Out,Err}() here results in a hang until something is input. // Using dockerCli.{In,Out,Err}() here results in a hang until something is input.
// See: - https://github.com/golang/go/issues/10338 // See: - https://github.com/golang/go/issues/10338
// - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab // - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab
@@ -221,15 +200,10 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Env = append(cmd.Environ(), metadata.ReexecEnvvar+"="+os.Args[0]) cmd.Env = os.Environ()
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin) cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
return cmd, nil return cmd, nil
} }
return nil, errPluginNotFound(name) return nil, errPluginNotFound(name)
} }
// IsPluginCommand checks if the given cmd is a plugin-stub.
func IsPluginCommand(cmd *cobra.Command) bool {
return cmd.Annotations[metadata.CommandAnnotationPlugin] == "true"
}
+21 -95
View File
@@ -1,17 +1,14 @@
package manager package manager
import ( import (
"path/filepath"
"strings" "strings"
"testing" "testing"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/spf13/cobra" "gotest.tools/assert"
"gotest.tools/v3/assert" "gotest.tools/fs"
"gotest.tools/v3/fs"
) )
func TestListPluginCandidates(t *testing.T) { func TestListPluginCandidates(t *testing.T) {
@@ -38,7 +35,7 @@ func TestListPluginCandidates(t *testing.T) {
"plugins3-target", // Will be referenced as a symlink from below "plugins3-target", // Will be referenced as a symlink from below
fs.WithFile("docker-plugin1", ""), fs.WithFile("docker-plugin1", ""),
fs.WithDir("ignored3"), fs.WithDir("ignored3"),
fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is ignored fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is still a candidate (but would fail tests later)
fs.WithFile("non-plugin-symlinked", ""), // This shouldn't appear, but ... fs.WithFile("non-plugin-symlinked", ""), // This shouldn't appear, but ...
fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should. fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should.
), ),
@@ -48,12 +45,13 @@ func TestListPluginCandidates(t *testing.T) {
) )
defer dir.Remove() defer dir.Remove()
dirs := make([]string, 0, 6) var dirs []string
for _, d := range []string{"plugins1", "nonexistent", "plugins2", "plugins3", "plugins4", "plugins5"} { for _, d := range []string{"plugins1", "nonexistent", "plugins2", "plugins3", "plugins4", "plugins5"} {
dirs = append(dirs, dir.Join(d)) dirs = append(dirs, dir.Join(d))
} }
candidates := listPluginCandidates(dirs) candidates, err := listPluginCandidates(dirs)
assert.NilError(t, err)
exp := map[string][]string{ exp := map[string][]string{
"plugin1": { "plugin1": {
dir.Join("plugins1", "docker-plugin1"), dir.Join("plugins1", "docker-plugin1"),
@@ -72,6 +70,9 @@ func TestListPluginCandidates(t *testing.T) {
"hardlink2": { "hardlink2": {
dir.Join("plugins2", "docker-hardlink2"), dir.Join("plugins2", "docker-hardlink2"),
}, },
"brokensymlink": {
dir.Join("plugins3", "docker-brokensymlink"),
},
"symlinked": { "symlinked": {
dir.Join("plugins3", "docker-symlinked"), dir.Join("plugins3", "docker-symlinked"),
}, },
@@ -80,109 +81,34 @@ func TestListPluginCandidates(t *testing.T) {
assert.DeepEqual(t, candidates, exp) assert.DeepEqual(t, candidates, exp)
} }
func TestListPluginCandidatesEmpty(t *testing.T) {
tmpDir := t.TempDir()
candidates := listPluginCandidates([]string{tmpDir, filepath.Join(tmpDir, "no-such-dir")})
assert.Assert(t, len(candidates) == 0)
}
// Regression test for https://github.com/docker/cli/issues/5643.
// Check that inaccessible directories that come before accessible ones are ignored
// and do not prevent the latter from being processed.
func TestListPluginCandidatesInaccessibleDir(t *testing.T) {
dir := fs.NewDir(t, t.Name(),
fs.WithDir("no-perm", fs.WithMode(0)),
fs.WithDir("plugins",
fs.WithFile("docker-buildx", ""),
),
)
defer dir.Remove()
candidates := listPluginCandidates([]string{
dir.Join("no-perm"),
dir.Join("plugins"),
})
assert.DeepEqual(t, candidates, map[string][]string{
"buildx": {
dir.Join("plugins", "docker-buildx"),
},
})
}
func TestGetPlugin(t *testing.T) {
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, errdefs.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) { func TestErrPluginNotFound(t *testing.T) {
var err error = errPluginNotFound("test") var err error = errPluginNotFound("test")
err.(errPluginNotFound).NotFound() err.(errPluginNotFound).NotFound()
assert.Error(t, err, "Error: No such CLI plugin: test") assert.Error(t, err, "Error: No such CLI plugin: test")
assert.Assert(t, errdefs.IsNotFound(err)) assert.Assert(t, IsNotFound(err))
assert.Assert(t, !errdefs.IsNotFound(nil)) assert.Assert(t, !IsNotFound(nil))
} }
func TestGetPluginDirs(t *testing.T) { func TestGetPluginDirs(t *testing.T) {
cli := test.NewFakeCli(nil) cli := test.NewFakeCli(nil)
pluginDir := filepath.Join(config.Dir(), "cli-plugins") pluginDir, err := config.Path("cli-plugins")
assert.NilError(t, err)
expected := append([]string{pluginDir}, defaultSystemPluginDirs...) expected := append([]string{pluginDir}, defaultSystemPluginDirs...)
pluginDirs := getPluginDirs(cli.ConfigFile()) var pluginDirs []string
pluginDirs, err = getPluginDirs(cli)
assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":")) assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))
assert.NilError(t, err)
extras := make([]string, 0, 3+len(expected)) extras := []string{
extras = append(extras, "foo", "bar", "baz") "foo", "bar", "baz",
}
expected = append(extras, expected...) expected = append(extras, expected...)
cli.SetConfigFile(&configfile.ConfigFile{ cli.SetConfigFile(&configfile.ConfigFile{
CLIPluginsExtraDirs: extras, CLIPluginsExtraDirs: extras,
}) })
pluginDirs = getPluginDirs(cli.ConfigFile()) pluginDirs, err = getPluginDirs(cli)
assert.DeepEqual(t, expected, pluginDirs) assert.DeepEqual(t, expected, pluginDirs)
assert.NilError(t, err)
} }
+3 -15
View File
@@ -1,20 +1,8 @@
//go:build !windows // +build !windows
package manager 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{ var defaultSystemPluginDirs = []string{
"/usr/local/lib/docker/cli-plugins", "/usr/local/lib/docker/cli-plugins", "/usr/local/libexec/docker/cli-plugins",
"/usr/local/libexec/docker/cli-plugins", "/usr/lib/docker/cli-plugins", "/usr/libexec/docker/cli-plugins",
"/usr/lib/docker/cli-plugins",
"/usr/libexec/docker/cli-plugins",
} }
+1 -10
View File
@@ -5,16 +5,7 @@ import (
"path/filepath" "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{ var defaultSystemPluginDirs = []string{
filepath.Join(os.Getenv("ProgramData"), "Docker", "cli-plugins"),
filepath.Join(os.Getenv("ProgramFiles"), "Docker", "cli-plugins"), filepath.Join(os.Getenv("ProgramFiles"), "Docker", "cli-plugins"),
} }
+28
View File
@@ -0,0 +1,28 @@
package manager
const (
// NamePrefix is the prefix required on all plugin binary names
NamePrefix = "docker-"
// MetadataSubcommandName is the name of the plugin subcommand
// which must be supported by every plugin and returns the
// plugin metadata.
MetadataSubcommandName = "docker-cli-plugin-metadata"
)
// 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"`
// Vendor is the name of the plugin vendor. Mandatory
Vendor string `json:",omitempty"`
// Version is the optional version of this plugin.
Version string `json:",omitempty"`
// ShortDescription should be suitable for a single line help message.
ShortDescription string `json:",omitempty"`
// URL is a pointer to the plugin's homepage.
URL string `json:",omitempty"`
// Experimental specifies whether the plugin is experimental.
// Experimental plugins are not displayed on non-experimental CLIs.
Experimental bool `json:",omitempty"`
}
+40 -130
View File
@@ -1,28 +1,22 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package manager package manager
import ( import (
"context"
"encoding"
"encoding/json" "encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath" "path/filepath"
"strconv" "regexp"
"strings" "strings"
"github.com/docker/cli/cli-plugins/hooks" "github.com/pkg/errors"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var (
pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
)
// Plugin represents a potential plugin with all it's metadata. // Plugin represents a potential plugin with all it's metadata.
type Plugin struct { type Plugin struct {
metadata.Metadata Metadata
Name string `json:",omitempty"` Name string `json:",omitempty"`
Path string `json:",omitempty"` Path string `json:",omitempty"`
@@ -34,34 +28,14 @@ type Plugin struct {
ShadowedPaths []string `json:",omitempty"` ShadowedPaths []string `json:",omitempty"`
} }
// MarshalJSON implements [json.Marshaler] to handle marshaling the
// [Plugin.Err] field (Go doesn't marshal errors by default).
func (p *Plugin) MarshalJSON() ([]byte, error) {
type Alias Plugin // avoid recursion
cp := *p // shallow copy to avoid mutating original
if cp.Err != nil {
if _, ok := cp.Err.(encoding.TextMarshaler); !ok {
cp.Err = &pluginError{cp.Err}
}
}
return json.Marshal((*Alias)(&cp))
}
// pluginCandidate represents a possible plugin candidate, for mocking purposes.
type pluginCandidate interface {
Path() string
Metadata() ([]byte, error)
}
// newPlugin determines if the given candidate is valid and returns a // newPlugin determines if the given candidate is valid and returns a
// Plugin. If the candidate fails one of the tests then `Plugin.Err` // Plugin. If the candidate fails one of the tests then `Plugin.Err`
// is set, and is always a `pluginError`, but the `Plugin` is still // is set, and is always a `pluginError`, but the `Plugin` is still
// returned with no error. An error is only returned due to a // returned with no error. An error is only returned due to a
// non-recoverable error. // non-recoverable error.
func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) { //
// nolint: gocyclo
func newPlugin(c Candidate, rootcmd *cobra.Command, allowExperimental bool) (Plugin, error) {
path := c.Path() path := c.Path()
if path == "" { if path == "" {
return Plugin{}, errors.New("plugin candidate path cannot be empty") return Plugin{}, errors.New("plugin candidate path cannot be empty")
@@ -71,41 +45,43 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
// which would fail here, so there are all real errors. // which would fail here, so there are all real errors.
fullname := filepath.Base(path) fullname := filepath.Base(path)
if fullname == "." { if fullname == "." {
return Plugin{}, fmt.Errorf("unable to determine basename of plugin candidate %q", path) return Plugin{}, errors.Errorf("unable to determine basename of plugin candidate %q", path)
} }
var err error var err error
if fullname, err = trimExeSuffix(fullname); err != nil { if fullname, err = trimExeSuffix(fullname); err != nil {
return Plugin{}, fmt.Errorf("plugin candidate %q: %w", path, err) return Plugin{}, errors.Wrapf(err, "plugin candidate %q", path)
} }
if !strings.HasPrefix(fullname, metadata.NamePrefix) { if !strings.HasPrefix(fullname, NamePrefix) {
return Plugin{}, fmt.Errorf("plugin candidate %q: does not have %q prefix", path, metadata.NamePrefix) return Plugin{}, errors.Errorf("plugin candidate %q: does not have %q prefix", path, NamePrefix)
} }
p := Plugin{ p := Plugin{
Name: strings.TrimPrefix(fullname, metadata.NamePrefix), Name: strings.TrimPrefix(fullname, NamePrefix),
Path: path, Path: path,
} }
// Now apply the candidate tests, so these update p.Err. // Now apply the candidate tests, so these update p.Err.
if !isValidPluginName(p.Name) { if !pluginNameRe.MatchString(p.Name) {
p.Err = newPluginError("plugin candidate %q did not match %q", p.Name, pluginNameFormat) p.Err = NewPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String())
return p, nil return p, nil
} }
for _, cmd := range cmds { if rootcmd != nil {
// Ignore conflicts with commands which are for _, cmd := range rootcmd.Commands() {
// just plugin stubs (i.e. from a previous // Ignore conflicts with commands which are
// call to AddPluginCommandStubs). // just plugin stubs (i.e. from a previous
if IsPluginCommand(cmd) { // call to AddPluginCommandStubs).
continue if p := cmd.Annotations[CommandAnnotationPlugin]; p == "true" {
} continue
if cmd.Name() == p.Name { }
p.Err = newPluginError("plugin %q duplicates builtin command", p.Name) if cmd.Name() == p.Name {
return p, nil 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()) if cmd.HasAlias(p.Name) {
return p, nil p.Err = NewPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name())
return p, nil
}
} }
} }
@@ -120,83 +96,17 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
p.Err = wrapAsPluginError(err, "invalid metadata") p.Err = wrapAsPluginError(err, "invalid metadata")
return p, nil return p, nil
} }
if err := validateSchemaVersion(p.Metadata.SchemaVersion); err != nil { if p.Experimental && !allowExperimental {
p.Err = &pluginError{cause: err} 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 return p, nil
} }
if p.Metadata.Vendor == "" { if p.Metadata.Vendor == "" {
p.Err = newPluginError("plugin metadata does not define a vendor") p.Err = NewPluginError("plugin metadata does not define a vendor")
return p, nil return p, nil
} }
return p, nil return p, nil
} }
// validateSchemaVersion validates if the plugin's schemaVersion is supported.
//
// The current schema-version is "0.1.0", but we don't want to break compatibility
// until v2.0.0 of the schema version. Check for the major version to be < 2.0.0.
//
// Note that CLI versions before 28.4.1 may not support these versions as they were
// hard-coded to only accept "0.1.0".
func validateSchemaVersion(version string) error {
if version == "0.1.0" {
return nil
}
if version == "" {
return errors.New("plugin SchemaVersion version cannot be empty")
}
major, _, ok := strings.Cut(version, ".")
majorVersion, err := strconv.Atoi(major)
if !ok || err != nil {
return fmt.Errorf("plugin SchemaVersion %q has wrong format: must be <major>.<minor>.<patch>", version)
}
if majorVersion > 1 {
return fmt.Errorf("plugin SchemaVersion %q is not supported: must be lower than 2.0.0", version)
}
return nil
}
// RunHook executes the plugin's hooks command
// and returns its unprocessed output.
func (p *Plugin) RunHook(ctx context.Context, hookData hooks.Request) ([]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, metadata.HookSubcommandName, string(hDataBytes)) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
pCmd.Env = os.Environ()
pCmd.Env = append(pCmd.Env, metadata.ReexecEnvvar+"="+os.Args[0])
out, err := pCmd.Output()
if err != nil {
if _, ok := errors.AsType[*exec.ExitError](err); ok {
return nil, wrapAsPluginError(err, "plugin hook subcommand exited unsuccessfully")
}
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand: "+pCmd.String())
}
return out, nil
}
// pluginNameFormat is used as part of errors for invalid plugin-names.
// We should consider making this less technical ("must start with "a-z",
// and only consist of lowercase alphanumeric characters").
const pluginNameFormat = `^[a-z][a-z0-9]*$`
func isValidPluginName(s string) bool {
if len(s) == 0 {
return false
}
// first character must be a-z
if c := s[0]; c < 'a' || c > 'z' {
return false
}
// followed by a-z or 0-9
for i := 1; i < len(s); i++ {
c := s[i]
if (c < 'a' || c > 'z') && (c < '0' || c > '9') {
return false
}
}
return true
}
-43
View File
@@ -1,43 +0,0 @@
package manager
import (
"encoding/json"
"errors"
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestPluginMarshal(t *testing.T) {
const jsonWithError = `{"Name":"some-plugin","Err":"something went wrong"}`
const jsonNoError = `{"Name":"some-plugin"}`
tests := []struct {
doc string
error error
expected string
}{
{
doc: "no error",
expected: jsonNoError,
},
{
doc: "regular error",
error: errors.New("something went wrong"),
expected: jsonWithError,
},
{
doc: "custom error",
error: newPluginError("something went wrong"),
expected: jsonWithError,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
actual, err := json.Marshal(&Plugin{Name: "some-plugin", Err: tc.error})
assert.NilError(t, err)
assert.Check(t, is.Equal(string(actual), tc.expected))
})
}
}
+1 -2
View File
@@ -1,11 +1,10 @@
//go:build !windows // +build !windows
package manager package manager
func trimExeSuffix(s string) (string, error) { func trimExeSuffix(s string) (string, error) {
return s, nil return s, nil
} }
func addExeSuffix(s string) string { func addExeSuffix(s string) string {
return s return s
} }
+10 -4
View File
@@ -1,16 +1,22 @@
package manager package manager
import ( import (
"fmt"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/pkg/errors"
) )
// This is made slightly more complex due to needing to be case-insensitive. // This is made slightly more complex due to needing to be case insensitive.
func trimExeSuffix(s string) (string, error) { func trimExeSuffix(s string) (string, error) {
ext := filepath.Ext(s) ext := filepath.Ext(s)
if ext == "" || !strings.EqualFold(ext, ".exe") { if ext == "" {
return "", fmt.Errorf("path %q lacks required file extension (.exe)", s) return "", errors.Errorf("path %q lacks required file extension", s)
}
exe := ".exe"
if !strings.EqualFold(ext, exe) {
return "", errors.Errorf("path %q lacks required %q suffix", s, exe)
} }
return strings.TrimSuffix(s, ext), nil return strings.TrimSuffix(s, ext), nil
} }
-85
View File
@@ -1,85 +0,0 @@
package manager
import (
"fmt"
"os"
"strings"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/baggage"
)
const (
// resourceAttributesEnvVar is the name of the envvar that includes additional
// resource attributes for OTEL as defined in the [OpenTelemetry specification].
//
// [OpenTelemetry specification]: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration
resourceAttributesEnvVar = "OTEL_RESOURCE_ATTRIBUTES"
// dockerCLIAttributePrefix is the prefix for any docker cli OTEL attributes.
//
// It is a copy of the const defined in [command.dockerCLIAttributePrefix].
dockerCLIAttributePrefix = "docker.cli."
cobraCommandPath = attribute.Key("cobra.command_path")
)
func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
commandPath := cmd.Annotations[metadata.CommandAnnotationPluginCommandPath]
if commandPath == "" {
commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name)
}
attrSet := attribute.NewSet(
cobraCommandPath.String(commandPath),
)
kvs := make([]attribute.KeyValue, 0, attrSet.Len())
for iter := attrSet.Iter(); iter.Next(); {
attr := iter.Attribute()
kvs = append(kvs, attribute.KeyValue{
Key: dockerCLIAttributePrefix + attr.Key,
Value: attr.Value,
})
}
return attribute.NewSet(kvs...)
}
func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string {
if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 {
// Construct baggage members for each of the attributes.
// Ignore any failures as these aren't significant and
// represent an internal issue.
members := make([]baggage.Member, 0, attrs.Len())
for iter := attrs.Iter(); iter.Next(); {
attr := iter.Attribute()
m, err := baggage.NewMemberRaw(string(attr.Key), attr.Value.AsString())
if err != nil {
otel.Handle(err)
continue
}
members = append(members, m)
}
// Combine plugin added resource attributes with ones found in the environment
// variable. Our own attributes should be namespaced so there shouldn't be a
// conflict. We do not parse the environment variable because we do not want
// to handle errors in user configuration.
attrsSlice := make([]string, 0, 2)
if v := strings.TrimSpace(os.Getenv(resourceAttributesEnvVar)); v != "" {
attrsSlice = append(attrsSlice, v)
}
if b, err := baggage.New(members...); err != nil {
otel.Handle(err)
} else if b.Len() > 0 {
attrsSlice = append(attrsSlice, b.String())
}
if len(attrsSlice) > 0 {
env = append(env, resourceAttributesEnvVar+"="+strings.Join(attrsSlice, ","))
}
}
return env
}
-28
View File
@@ -1,28 +0,0 @@
package metadata
const (
// CommandAnnotationPlugin is added to every stub command added by
// AddPluginCommandStubs with the value "true" and so can be
// used to distinguish plugin stubs from regular commands.
CommandAnnotationPlugin = "com.docker.cli.plugin"
// CommandAnnotationPluginVendor is added to every stub command
// added by AddPluginCommandStubs and contains the vendor of
// that plugin.
CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor"
// CommandAnnotationPluginVersion is added to every stub command
// added by AddPluginCommandStubs and contains the version of
// that plugin.
CommandAnnotationPluginVersion = "com.docker.cli.plugin.version"
// CommandAnnotationPluginInvalid is added to any stub command
// added by AddPluginCommandStubs for an invalid command (that
// is, one which failed it's candidate test) and contains the
// reason for the failure.
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
// CommandAnnotationPluginCommandPath is added to overwrite the
// command path for a plugin invocation.
CommandAnnotationPluginCommandPath = "com.docker.cli.plugin.command_path"
)
-38
View File
@@ -1,38 +0,0 @@
package metadata
const (
// NamePrefix is the prefix required on all plugin binary names
NamePrefix = "docker-"
// MetadataSubcommandName is the name of the plugin subcommand
// which must be supported by every plugin and returns the
// plugin metadata.
MetadataSubcommandName = "docker-cli-plugin-metadata"
// HookSubcommandName is the name of the plugin subcommand
// which must be implemented by plugins declaring support
// for hooks in their metadata.
HookSubcommandName = "docker-cli-plugin-hooks"
// ReexecEnvvar is the name of an ennvar which is set to the command
// used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI.
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
)
// Metadata provided by the plugin.
type Metadata struct {
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
SchemaVersion string `json:",omitempty"`
// Vendor is the name of the plugin vendor. Mandatory
Vendor string `json:",omitempty"`
// Version is the optional version of this plugin.
Version string `json:",omitempty"`
// ShortDescription should be suitable for a single line help message.
ShortDescription string `json:",omitempty"`
// URL is a pointer to the plugin's homepage.
URL string `json:",omitempty"`
// Hidden hides the plugin in completion and help message output.
Hidden bool `json:",omitempty"`
}
+38 -111
View File
@@ -1,76 +1,43 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package plugin package plugin
import ( import (
"context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"sync" "sync"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/socket"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/connhelper" "github.com/docker/cli/cli/connhelper"
"github.com/docker/cli/cli/debug" "github.com/docker/docker/client"
"github.com/moby/moby/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.opentelemetry.io/otel"
) )
// PersistentPreRunE must be called by any plugin command (or // PersistentPreRunE must be called by any plugin command (or
// subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins // subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins
// which do not make use of `PersistentPreRun*` do not need to call // which do not make use of `PersistentPreRun*` do not need to call
// this (although it remains safe to do so). Plugins are recommended // 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 // returned. Should not be called outside of a command's
// PersistentPreRunE hook and must not be run unless Run has been // PersistentPreRunE hook and must not be run unless Run has been
// called. // called.
var PersistentPreRunE func(*cobra.Command, []string) error 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 metadata.Metadata) error {
tcmd := newPluginCommand(dockerCli, plugin, meta) tcmd := newPluginCommand(dockerCli, plugin, meta)
var persistentPreRunOnce sync.Once var persistentPreRunOnce sync.Once
PersistentPreRunE = func(cmd *cobra.Command, _ []string) error { PersistentPreRunE = func(_ *cobra.Command, _ []string) error {
var retErr error var err error
persistentPreRunOnce.Do(func() { persistentPreRunOnce.Do(func() {
ctx, cancel := context.WithCancel(cmd.Context()) var opts []command.InitializeOpt
cmd.SetContext(ctx)
// Set up the context to cancel based on signalling via CLI socket.
socket.ConnectAndWait(cancel)
var opts []command.CLIOption
if os.Getenv("DOCKER_CLI_PLUGIN_USE_DIAL_STDIO") != "" { if os.Getenv("DOCKER_CLI_PLUGIN_USE_DIAL_STDIO") != "" {
opts = append(opts, withPluginClientConn(plugin.Name())) opts = append(opts, withPluginClientConn(plugin.Name()))
} }
opts = append(opts, command.WithEnableGlobalMeterProvider(), command.WithEnableGlobalTracerProvider()) err = tcmd.Initialize(opts...)
retErr = 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 retErr return err
} }
cmd, args, err := tcmd.HandleGlobalFlags() cmd, args, err := tcmd.HandleGlobalFlags()
@@ -83,41 +50,37 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
return cmd.Execute() return cmd.Execute()
} }
// Run is the top-level entry point to the CLI plugin framework. It should // Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
// be called from the plugin's "main()" function. It initializes a new func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
// [command.DockerCli] instance with the given options before calling dockerCli, err := command.NewDockerCli()
// makeCmd to construct the plugin command, then invokes the plugin command
// using [RunPlugin].
func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata, ops ...command.CLIOption) {
otel.SetErrorHandler(debug.OTELErrorHandler)
dockerCLI, err := command.NewDockerCli(ops...)
if err != nil { if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
os.Exit(1) os.Exit(1)
} }
plugin := makeCmd(dockerCLI) plugin := makeCmd(dockerCli)
if err := RunPlugin(dockerCLI, plugin, meta); err != nil { if err := runPlugin(dockerCli, plugin, meta); err != nil {
if stErr, ok := errors.AsType[cli.StatusError](err); ok { if sterr, ok := err.(cli.StatusError); ok {
if sterr.Status != "" {
fmt.Fprintln(dockerCli.Err(), sterr.Status)
}
// StatusError should only be used for errors, and all errors should // StatusError should only be used for errors, and all errors should
// have a non-zero exit status, so never exit with 0 // have a non-zero exit status, so never exit with 0
if stErr.StatusCode == 0 { // FIXME(thaJeztah): this should never be used with a zero status-code. Check if we do this anywhere. if sterr.StatusCode == 0 {
stErr.StatusCode = 1 os.Exit(1)
} }
_, _ = fmt.Fprintln(dockerCLI.Err(), stErr) os.Exit(sterr.StatusCode)
os.Exit(stErr.StatusCode)
} }
_, _ = fmt.Fprintln(dockerCLI.Err(), err) fmt.Fprintln(dockerCli.Err(), err)
os.Exit(1) os.Exit(1)
} }
} }
func withPluginClientConn(name string) command.CLIOption { func withPluginClientConn(name string) command.InitializeOpt {
return func(cli *command.DockerCli) error { return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) {
cmd := "docker" cmd := "docker"
if x := os.Getenv(metadata.ReexecEnvvar); x != "" { if x := os.Getenv(manager.ReexecEnvvar); x != "" {
cmd = x cmd = x
} }
var flags []string var flags []string
@@ -139,19 +102,16 @@ func withPluginClientConn(name string) command.CLIOption {
helper, err := connhelper.GetCommandConnectionHelper(cmd, flags...) helper, err := connhelper.GetCommandConnectionHelper(cmd, flags...)
if err != nil { if err != nil {
return err return nil, err
} }
apiClient, err := client.New(client.WithDialContext(helper.Dialer))
if err != nil { return client.NewClientWithOpts(client.WithDialContext(helper.Dialer))
return err })
}
return command.WithAPIClient(apiClient)(cli)
}
} }
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) *cli.TopLevelCommand { func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cli.TopLevelCommand {
name := plugin.Name() name := plugin.Name()
fullname := metadata.NamePrefix + name fullname := manager.NamePrefix + name
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name), Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name),
@@ -164,52 +124,27 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
}, },
TraverseChildren: true, TraverseChildren: true,
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: false,
HiddenDefaultCmd: true,
DisableDescriptions: os.Getenv("DOCKER_CLI_DISABLE_COMPLETION_DESCRIPTION") != "",
},
} }
opts, flags := cli.SetupPluginRootCommand(cmd)
// Disable file-completion by default. Most commands and flags should not cmd.SetOutput(dockerCli.Out())
// complete with filenames.
cmd.CompletionOptions.SetDefaultShellCompDirective(cobra.ShellCompDirectiveNoFileComp)
opts, _ := cli.SetupPluginRootCommand(cmd)
cmd.SetIn(dockerCli.In())
cmd.SetOut(dockerCli.Out())
cmd.SetErr(dockerCli.Err())
cmd.AddCommand( cmd.AddCommand(
plugin, plugin,
newMetadataSubcommand(plugin, meta), newMetadataSubcommand(plugin, meta),
) )
visitAll(cmd, cli.DisableFlagsInUseLine(cmd)
// prevent adding "[flags]" to the end of the usage line.
func(c *cobra.Command) { c.DisableFlagsInUseLine = true },
)
return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags()) return cli.NewTopLevelCommand(cmd, dockerCli, opts, flags)
} }
// visitAll traverses all commands from the root. func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
func visitAll(root *cobra.Command, fns ...func(*cobra.Command)) {
for _, cmd := range root.Commands() {
visitAll(cmd, fns...)
}
for _, fn := range fns {
fn(root)
}
}
func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra.Command {
if meta.ShortDescription == "" { if meta.ShortDescription == "" {
meta.ShortDescription = plugin.Short meta.ShortDescription = plugin.Short
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: metadata.MetadataSubcommandName, Use: manager.MetadataSubcommandName,
Hidden: true, Hidden: true,
// Suppress the global/parent PersistentPreRunE, which // Suppress the global/parent PersistentPreRunE, which
// needlessly initializes the client and tries to // needlessly initializes the client and tries to
@@ -224,11 +159,3 @@ func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra
} }
return cmd return cmd
} }
// RunningStandalone tells a CLI plugin it is run standalone by direct execution
func RunningStandalone() bool {
if os.Getenv(metadata.ReexecEnvvar) != "" {
return false
}
return len(os.Args) < 2 || os.Args[1] != metadata.MetadataSubcommandName
}
-28
View File
@@ -1,28 +0,0 @@
package plugin
import (
"slices"
"testing"
"github.com/spf13/cobra"
)
func TestVisitAll(t *testing.T) {
root := &cobra.Command{Use: "root"}
sub1 := &cobra.Command{Use: "sub1"}
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
sub2 := &cobra.Command{Use: "sub2"}
root.AddCommand(sub1, sub2)
sub1.AddCommand(sub1sub1, sub1sub2)
var visited []string
visitAll(root, func(ccmd *cobra.Command) {
visited = append(visited, ccmd.Name())
})
expected := []string{"sub1sub1", "sub1sub2", "sub1", "sub2", "root"}
if !slices.Equal(expected, visited) {
t.Errorf("expected %#v, got %#v", expected, visited)
}
}
-169
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.Listener.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
}
}
}()
}
-9
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
}
-14
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)
}
-216
View File
@@ -1,216 +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("expected EOF error, got: %v", err)
}
case <-time.After(10 * time.Millisecond):
}
})
t.Run("allows reconnects", func(t *testing.T) {
var calls atomic.Int32
h := func(_ net.Conn) {
calls.Add(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 calls.Load() == 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) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
srv, err := NewPluginServer(nil)
assert.NilError(t, err, "failed to setup server")
defer srv.Close()
t.Setenv(EnvKey, srv.Addr().String())
runtime.Gosched()
numGoroutines := runtime.NumGoroutine()
ConnectAndWait(func() {})
runtime.Gosched()
poll.WaitOn(t, func(t poll.LogT) poll.Result {
// +1 goroutine for the poll.WaitOn
// +1 goroutine for the connect goroutine
if runtime.NumGoroutine() < numGoroutines+1+1 {
return poll.Continue("waiting for connect goroutine to spawn")
}
return poll.Success()
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(500*time.Millisecond))
srv.Close()
runtime.Gosched()
poll.WaitOn(t, func(t poll.LogT) poll.Result {
// +1 goroutine for the poll.WaitOn
if runtime.NumGoroutine() > numGoroutines+1 {
return poll.Continue("waiting for connect goroutine to exit")
}
return poll.Success()
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(500*time.Millisecond))
})
}
+65 -206
View File
@@ -1,50 +1,40 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package cli package cli
import ( import (
"fmt" "fmt"
"os" "os"
"slices"
"strings" "strings"
"github.com/docker/cli/cli-plugins/metadata" pluginmanager "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
cliconfig "github.com/docker/cli/cli/config"
cliflags "github.com/docker/cli/cli/flags" cliflags "github.com/docker/cli/cli/flags"
"github.com/fvbommel/sortorder" "github.com/docker/docker/pkg/term"
"github.com/moby/term" "github.com/pkg/errors"
"github.com/morikuni/aec"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
// setupCommonRootCommand contains the setup common to // setupCommonRootCommand contains the setup common to
// SetupRootCommand and SetupPluginRootCommand. // 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 := 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("add", func(a, b int) int { return a + b })
cobra.AddTemplateFunc("hasAliases", hasAliases)
cobra.AddTemplateFunc("hasSubCommands", hasSubCommands) cobra.AddTemplateFunc("hasSubCommands", hasSubCommands)
cobra.AddTemplateFunc("hasTopCommands", hasTopCommands)
cobra.AddTemplateFunc("hasManagementSubCommands", hasManagementSubCommands) cobra.AddTemplateFunc("hasManagementSubCommands", hasManagementSubCommands)
cobra.AddTemplateFunc("hasSwarmSubCommands", hasSwarmSubCommands)
cobra.AddTemplateFunc("hasInvalidPlugins", hasInvalidPlugins) cobra.AddTemplateFunc("hasInvalidPlugins", hasInvalidPlugins)
cobra.AddTemplateFunc("topCommands", topCommands)
cobra.AddTemplateFunc("commandAliases", commandAliases)
cobra.AddTemplateFunc("operationSubCommands", operationSubCommands) cobra.AddTemplateFunc("operationSubCommands", operationSubCommands)
cobra.AddTemplateFunc("managementSubCommands", managementSubCommands) cobra.AddTemplateFunc("managementSubCommands", managementSubCommands)
cobra.AddTemplateFunc("orchestratorSubCommands", orchestratorSubCommands)
cobra.AddTemplateFunc("invalidPlugins", invalidPlugins) cobra.AddTemplateFunc("invalidPlugins", invalidPlugins)
cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages) cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages)
cobra.AddTemplateFunc("vendorAndVersion", vendorAndVersion) cobra.AddTemplateFunc("vendorAndVersion", vendorAndVersion)
cobra.AddTemplateFunc("invalidPluginReason", invalidPluginReason) cobra.AddTemplateFunc("invalidPluginReason", invalidPluginReason)
cobra.AddTemplateFunc("isPlugin", isPlugin) cobra.AddTemplateFunc("isPlugin", isPlugin)
cobra.AddTemplateFunc("isExperimental", isExperimental)
cobra.AddTemplateFunc("hasAdditionalHelp", hasAdditionalHelp)
cobra.AddTemplateFunc("additionalHelp", additionalHelp)
cobra.AddTemplateFunc("decoratedName", decoratedName) cobra.AddTemplateFunc("decoratedName", decoratedName)
rootCmd.SetUsageTemplate(usageTemplate) rootCmd.SetUsageTemplate(usageTemplate)
@@ -53,28 +43,26 @@ func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *c
rootCmd.SetHelpCommand(helpCommand) rootCmd.SetHelpCommand(helpCommand)
rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage") 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.PersistentFlags().Lookup("help").Hidden = true
rootCmd.Annotations = map[string]string{ return opts, flags, helpCommand
"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
}
return opts, helpCommand
} }
// SetupRootCommand sets default usage, help, and error handling for the // SetupRootCommand sets default usage, help, and error handling for the
// root command. // 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") 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. // SetupPluginRootCommand sets default usage, help and error handling for a plugin root command.
func SetupPluginRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) { func SetupPluginRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) {
opts, _ := setupCommonRootCommand(rootCmd) opts, flags, _ := setupCommonRootCommand(rootCmd)
return opts, rootCmd.Flags() return opts, flags
} }
// FlagErrorFunc prints an error message which matches the format of the // FlagErrorFunc prints an error message which matches the format of the
@@ -84,8 +72,12 @@ func FlagErrorFunc(cmd *cobra.Command, err error) error {
return nil return nil
} }
usage := ""
if cmd.HasSubCommands() {
usage = "\n\n" + cmd.UsageString()
}
return StatusError{ return StatusError{
Status: fmt.Sprintf("%s\n\nUsage: %s\n\nRun '%s --help' for more information", err, cmd.UseLine(), cmd.CommandPath()), Status: fmt.Sprintf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage),
StatusCode: 125, StatusCode: 125,
} }
} }
@@ -103,13 +95,7 @@ type TopLevelCommand struct {
// NewTopLevelCommand returns a new TopLevelCommand object // NewTopLevelCommand returns a new TopLevelCommand object
func NewTopLevelCommand(cmd *cobra.Command, dockerCli *command.DockerCli, opts *cliflags.ClientOptions, flags *pflag.FlagSet) *TopLevelCommand { func NewTopLevelCommand(cmd *cobra.Command, dockerCli *command.DockerCli, opts *cliflags.ClientOptions, flags *pflag.FlagSet) *TopLevelCommand {
return &TopLevelCommand{ return &TopLevelCommand{cmd, dockerCli, opts, flags, os.Args[1:]}
cmd: cmd,
dockerCli: dockerCli,
opts: opts,
flags: flags,
args: os.Args[1:],
}
} }
// SetArgs sets the args (default os.Args[:1] used to invoke the command // SetArgs sets the args (default os.Args[:1] used to invoke the command
@@ -164,11 +150,30 @@ func (tcmd *TopLevelCommand) HandleGlobalFlags() (*cobra.Command, []string, erro
} }
// Initialize finalises global option parsing and initializes the docker client. // Initialize finalises global option parsing and initializes the docker client.
func (tcmd *TopLevelCommand) Initialize(ops ...command.CLIOption) error { func (tcmd *TopLevelCommand) Initialize(ops ...command.InitializeOpt) error {
tcmd.opts.SetDefaultOptions(tcmd.flags) tcmd.opts.Common.SetDefaultOptions(tcmd.flags)
return tcmd.dockerCli.Initialize(tcmd.opts, ops...) return tcmd.dockerCli.Initialize(tcmd.opts, ops...)
} }
// VisitAll will traverse all commands from the root.
// This is different from the VisitAll of cobra.Command where only parents
// are checked.
func VisitAll(root *cobra.Command, fn func(*cobra.Command)) {
for _, cmd := range root.Commands() {
VisitAll(cmd, fn)
}
fn(root)
}
// DisableFlagsInUseLine sets the DisableFlagsInUseLine flag on all
// commands within the tree rooted at cmd.
func DisableFlagsInUseLine(cmd *cobra.Command) {
VisitAll(cmd, func(ccmd *cobra.Command) {
// do not add a `[flags]` to the end of the usage line.
ccmd.DisableFlagsInUseLine = true
})
}
var helpCommand = &cobra.Command{ var helpCommand = &cobra.Command{
Use: "help [command]", Use: "help [command]",
Short: "Help about the command", Short: "Help about the command",
@@ -177,49 +182,17 @@ var helpCommand = &cobra.Command{
RunE: func(c *cobra.Command, args []string) error { RunE: func(c *cobra.Command, args []string) error {
cmd, args, e := c.Root().Find(args) cmd, args, e := c.Root().Find(args)
if cmd == nil || e != nil || len(args) > 0 { if cmd == nil || e != nil || len(args) > 0 {
return fmt.Errorf("unknown help topic: %v", strings.Join(args, " ")) return errors.Errorf("unknown help topic: %v", strings.Join(args, " "))
} }
helpFunc := cmd.HelpFunc() helpFunc := cmd.HelpFunc()
helpFunc(cmd, args) helpFunc(cmd, args)
return nil 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 { func isPlugin(cmd *cobra.Command) bool {
return cmd.Annotations[metadata.CommandAnnotationPlugin] == "true" return cmd.Annotations[pluginmanager.CommandAnnotationPlugin] == "true"
}
func hasAliases(cmd *cobra.Command) bool {
return len(cmd.Aliases) > 0 || cmd.Annotations["aliases"] != ""
} }
func hasSubCommands(cmd *cobra.Command) bool { func hasSubCommands(cmd *cobra.Command) bool {
@@ -230,70 +203,16 @@ func hasManagementSubCommands(cmd *cobra.Command) bool {
return len(managementSubCommands(cmd)) > 0 return len(managementSubCommands(cmd)) > 0
} }
func hasSwarmSubCommands(cmd *cobra.Command) bool {
return len(orchestratorSubCommands(cmd)) > 0
}
func hasInvalidPlugins(cmd *cobra.Command) bool { func hasInvalidPlugins(cmd *cobra.Command) bool {
return len(invalidPlugins(cmd)) > 0 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() + " "
}
var aliases strings.Builder
aliases.WriteString(cmd.CommandPath())
for _, alias := range cmd.Aliases {
aliases.WriteString(", " + parentPath + alias)
}
return aliases.String()
}
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)
}
}
slices.SortStableFunc(cmds, func(a, b *cobra.Command) int {
return sortorder.NaturalCompare(a.Annotations["category-top"], b.Annotations["category-top"])
})
return cmds
}
func operationSubCommands(cmd *cobra.Command) []*cobra.Command { func operationSubCommands(cmd *cobra.Command) []*cobra.Command {
cmds := []*cobra.Command{} cmds := []*cobra.Command{}
for _, sub := range cmd.Commands() { for _, sub := range cmd.Commands() {
if isPlugin(sub) { if isPlugin(sub) {
continue 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() { if sub.IsAvailableCommand() && !sub.HasSubCommands() {
cmds = append(cmds, sub) cmds = append(cmds, sub)
} }
@@ -301,10 +220,8 @@ func operationSubCommands(cmd *cobra.Command) []*cobra.Command {
return cmds return cmds
} }
const defaultTermWidth = 80
func wrappedFlagUsages(cmd *cobra.Command) string { func wrappedFlagUsages(cmd *cobra.Command) string {
width := defaultTermWidth width := 80
if ws, err := term.GetWinsize(0); err == nil { if ws, err := term.GetWinsize(0); err == nil {
width = int(ws.Width) width = int(ws.Width)
} }
@@ -320,9 +237,9 @@ func decoratedName(cmd *cobra.Command) string {
} }
func vendorAndVersion(cmd *cobra.Command) string { func vendorAndVersion(cmd *cobra.Command) string {
if vendor, ok := cmd.Annotations[metadata.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) { if vendor, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) {
version := "" version := ""
if v, ok := cmd.Annotations[metadata.CommandAnnotationPluginVersion]; ok && v != "" { if v, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVersion]; ok && v != "" {
version = ", " + v version = ", " + v
} }
return fmt.Sprintf("(%s%s)", vendor, version) return fmt.Sprintf("(%s%s)", vendor, version)
@@ -331,33 +248,15 @@ func vendorAndVersion(cmd *cobra.Command) string {
} }
func managementSubCommands(cmd *cobra.Command) []*cobra.Command { 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{} cmds := []*cobra.Command{}
for _, sub := range cmd.Commands() { for _, sub := range cmd.Commands() {
if invalidPluginReason(sub) != "" { if isPlugin(sub) {
if invalidPluginReason(sub) == "" {
cmds = append(cmds, sub)
}
continue continue
} }
if sub.IsAvailableCommand() && (isPlugin(sub) || sub.HasSubCommands()) { if sub.IsAvailableCommand() && sub.HasSubCommands() {
cmds = append(cmds, sub) cmds = append(cmds, sub)
} }
} }
@@ -378,29 +277,20 @@ func invalidPlugins(cmd *cobra.Command) []*cobra.Command {
} }
func invalidPluginReason(cmd *cobra.Command) string { func invalidPluginReason(cmd *cobra.Command) string {
return cmd.Annotations[metadata.CommandAnnotationPluginInvalid] return cmd.Annotations[pluginmanager.CommandAnnotationPluginInvalid]
} }
const usageTemplate = `Usage: var usageTemplate = `Usage:
{{- if not .HasSubCommands}} {{.UseLine}}{{end}} {{- if not .HasSubCommands}} {{.UseLine}}{{end}}
{{- if .HasSubCommands}} {{ .CommandPath}}{{- if .HasAvailableFlags}} [OPTIONS]{{end}} COMMAND{{end}} {{- if .HasSubCommands}} {{ .CommandPath}}{{- if .HasAvailableFlags}} [OPTIONS]{{end}} COMMAND{{end}}
{{if ne .Long ""}}{{ .Long | trim }}{{ else }}{{ .Short | trim }}{{end}} {{if ne .Long ""}}{{ .Long | trim }}{{ else }}{{ .Short | trim }}{{end}}
{{- if isExperimental .}}
EXPERIMENTAL: {{- if gt .Aliases 0}}
{{.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 . }}
Aliases: Aliases:
{{ commandAliases . }} {{.NameAndAliases}}
{{- end}} {{- end}}
{{- if .HasExample}} {{- if .HasExample}}
@@ -409,36 +299,18 @@ Examples:
{{ .Example }} {{ .Example }}
{{- end}} {{- end}}
{{- if .HasParent}}
{{- if .HasAvailableFlags}} {{- if .HasAvailableFlags}}
Options: Options:
{{ wrappedFlagUsages . | trimRightSpace}} {{ wrappedFlagUsages . | trimRightSpace}}
{{- end}}
{{- end}}
{{- if hasTopCommands .}}
Common Commands:
{{- range topCommands .}}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
{{- end}}
{{- end}} {{- end}}
{{- if hasManagementSubCommands . }} {{- if hasManagementSubCommands . }}
Management Commands: Management Commands:
{{- range managementSubCommands . }} {{- range managementSubCommands . }}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}} {{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
{{- end}}
{{- end}}
{{- if hasSwarmSubCommands . }}
Swarm Commands:
{{- range orchestratorSubCommands . }}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
{{- end}} {{- end}}
{{- end}} {{- end}}
@@ -459,26 +331,13 @@ Invalid Plugins:
{{rpad .Name .NamePadding }} {{invalidPluginReason .}} {{rpad .Name .NamePadding }} {{invalidPluginReason .}}
{{- end}} {{- end}}
{{- end}}
{{- if not .HasParent}}
{{- if .HasAvailableFlags}}
Global Options:
{{ wrappedFlagUsages . | trimRightSpace}}
{{- end}}
{{- end}} {{- end}}
{{- if .HasSubCommands }} {{- if .HasSubCommands }}
Run '{{.CommandPath}} COMMAND --help' for more information on a command. Run '{{.CommandPath}} COMMAND --help' for more information on a command.
{{- end}}
{{- if hasAdditionalHelp .}}
{{ additionalHelp . }}
{{- end}} {{- end}}
` `
const helpTemplate = ` var helpTemplate = `
{{- if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` {{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
+31 -53
View File
@@ -3,13 +3,35 @@ package cli
import ( import (
"testing" "testing"
"github.com/docker/cli/cli-plugins/metadata" pluginmanager "github.com/docker/cli/cli-plugins/manager"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gotest.tools/v3/assert" "gotest.tools/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/assert/cmp"
) )
func TestVisitAll(t *testing.T) {
root := &cobra.Command{Use: "root"}
sub1 := &cobra.Command{Use: "sub1"}
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
sub2 := &cobra.Command{Use: "sub2"}
root.AddCommand(sub1, sub2)
sub1.AddCommand(sub1sub1, sub1sub2)
// Take the opportunity to test DisableFlagsInUseLine too
DisableFlagsInUseLine(root)
var visited []string
VisitAll(root, func(ccmd *cobra.Command) {
visited = append(visited, ccmd.Name())
assert.Assert(t, ccmd.DisableFlagsInUseLine, "DisableFlagsInUseLine not set on %q", ccmd.Name())
})
expected := []string{"sub1sub1", "sub1sub2", "sub1", "sub2", "root"}
assert.DeepEqual(t, expected, visited)
}
func TestVendorAndVersion(t *testing.T) { func TestVendorAndVersion(t *testing.T) {
// Non plugin. // Non plugin.
assert.Equal(t, vendorAndVersion(&cobra.Command{Use: "test"}), "") assert.Equal(t, vendorAndVersion(&cobra.Command{Use: "test"}), "")
@@ -27,9 +49,9 @@ func TestVendorAndVersion(t *testing.T) {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "test", Use: "test",
Annotations: map[string]string{ Annotations: map[string]string{
metadata.CommandAnnotationPlugin: "true", pluginmanager.CommandAnnotationPlugin: "true",
metadata.CommandAnnotationPluginVendor: tc.vendor, pluginmanager.CommandAnnotationPluginVendor: tc.vendor,
metadata.CommandAnnotationPluginVersion: tc.version, pluginmanager.CommandAnnotationPluginVersion: tc.version,
}, },
} }
assert.Equal(t, vendorAndVersion(cmd), tc.expected) assert.Equal(t, vendorAndVersion(cmd), tc.expected)
@@ -47,8 +69,8 @@ func TestInvalidPlugin(t *testing.T) {
assert.Assert(t, is.Len(invalidPlugins(root), 0)) assert.Assert(t, is.Len(invalidPlugins(root), 0))
sub1.Annotations = map[string]string{ sub1.Annotations = map[string]string{
metadata.CommandAnnotationPlugin: "true", pluginmanager.CommandAnnotationPlugin: "true",
metadata.CommandAnnotationPluginInvalid: "foo", pluginmanager.CommandAnnotationPluginInvalid: "foo",
} }
root.AddCommand(sub1, sub2) root.AddCommand(sub1, sub2)
sub1.AddCommand(sub1sub1, sub1sub2) sub1.AddCommand(sub1sub1, sub1sub2)
@@ -56,55 +78,11 @@ func TestInvalidPlugin(t *testing.T) {
assert.DeepEqual(t, invalidPlugins(root), []*cobra.Command{sub1}, cmpopts.IgnoreUnexported(cobra.Command{})) assert.DeepEqual(t, invalidPlugins(root), []*cobra.Command{sub1}, cmpopts.IgnoreUnexported(cobra.Command{}))
} }
func TestHiddenPlugin(t *testing.T) {
root := &cobra.Command{Use: "root"}
sub1 := &cobra.Command{
Use: "sub1",
Hidden: true,
Annotations: map[string]string{
metadata.CommandAnnotationPlugin: "true",
},
Run: func(cmd *cobra.Command, args []string) {},
}
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
sub2 := &cobra.Command{
Use: "sub2",
Annotations: map[string]string{
metadata.CommandAnnotationPlugin: "true",
},
Run: func(cmd *cobra.Command, args []string) {},
}
root.AddCommand(sub1, sub2)
sub1.AddCommand(sub1sub1, sub1sub2)
assert.DeepEqual(t, allManagementSubCommands(root), []*cobra.Command{sub2}, cmpopts.IgnoreFields(cobra.Command{}, "Run"), cmpopts.IgnoreUnexported(cobra.Command{}))
}
func TestCommandAliases(t *testing.T) {
root := &cobra.Command{Use: "root"}
sub := &cobra.Command{Use: "subcommand", Aliases: []string{"alias1", "alias2"}}
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) { func TestDecoratedName(t *testing.T) {
root := &cobra.Command{Use: "root"} root := &cobra.Command{Use: "root"}
topLevelCommand := &cobra.Command{Use: "pluginTopLevelCommand"} topLevelCommand := &cobra.Command{Use: "pluginTopLevelCommand"}
root.AddCommand(topLevelCommand) root.AddCommand(topLevelCommand)
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand ") assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand ")
topLevelCommand.Annotations = map[string]string{metadata.CommandAnnotationPlugin: "true"} topLevelCommand.Annotations = map[string]string{pluginmanager.CommandAnnotationPlugin: "true"}
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand*") assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand*")
} }
-19
View File
@@ -1,19 +0,0 @@
package builder
import (
"context"
"github.com/moby/moby/client"
)
type fakeClient struct {
client.Client
builderPruneFunc func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error)
}
func (c *fakeClient) BuildCachePrune(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
if c.builderPruneFunc != nil {
return c.builderPruneFunc(ctx, opts)
}
return client.BuildCachePruneResult{}, nil
}
+5 -37
View File
@@ -6,52 +6,20 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/command/image"
"github.com/docker/cli/internal/commands"
) )
func init() { // NewBuilderCommand returns a cobra command for `builder` subcommands
commands.Register(newBuilderCommand) func NewBuilderCommand(dockerCli command.Cli) *cobra.Command {
commands.Register(func(c command.Cli) *cobra.Command {
return newBakeStubCommand(c)
})
}
// newBuilderCommand returns a cobra command for `builder` subcommands
func newBuilderCommand(dockerCLI command.Cli) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "builder", Use: "builder",
Short: "Manage builds", Short: "Manage builds",
Args: cli.NoArgs, Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCLI.Err()), RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{"version": "1.31"}, Annotations: map[string]string{"version": "1.31"},
DisableFlagsInUseLine: true,
} }
cmd.AddCommand( cmd.AddCommand(
newPruneCommand(dockerCLI), NewPruneCommand(dockerCli),
// we should have a mechanism for registering sub-commands in the cli/internal/commands.Register function. image.NewBuildCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
image.NewBuildCommand(dockerCLI),
) )
return cmd return cmd
} }
// newBakeStubCommand returns a cobra command "stub" for the "bake" subcommand.
// This command is a placeholder / stub that is dynamically replaced by an
// alias for "docker buildx bake" if BuildKit is enabled (and the buildx plugin
// installed).
func newBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
return &cobra.Command{
Use: "bake [OPTIONS] [TARGET...]",
Short: "Build from a file",
RunE: command.ShowHelp(dockerCLI.Err()),
Annotations: map[string]string{
// We want to show this command in the "top" category in --help
// output, and not to be grouped under "management commands".
"category-top": "5",
"aliases": "docker buildx bake",
"version": "1.31",
},
DisableFlagsInUseLine: true,
}
}
+28 -64
View File
@@ -2,36 +2,26 @@ package builder
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/system/pruner"
"github.com/docker/cli/internal/prompt"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/docker/go-units" "github.com/docker/docker/api/types"
"github.com/moby/moby/client" units "github.com/docker/go-units"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func init() {
// Register the prune command to run as part of "docker system prune"
if err := pruner.Register(pruner.TypeBuildCache, pruneFn); err != nil {
panic(err)
}
}
type pruneOptions struct { type pruneOptions struct {
force bool force bool
all bool all bool
filter opts.FilterOpt filter opts.FilterOpt
reservedSpace opts.MemBytes keepStorage opts.MemBytes
} }
// newPruneCommand returns a new cobra prune command for images // NewPruneCommand returns a new cobra prune command for images
func newPruneCommand(dockerCLI command.Cli) *cobra.Command { func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
options := pruneOptions{filter: opts.NewFilterOpt()} options := pruneOptions{filter: opts.NewFilterOpt()}
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -39,26 +29,24 @@ func newPruneCommand(dockerCLI command.Cli) *cobra.Command {
Short: "Remove build cache", Short: "Remove build cache",
Args: cli.NoArgs, Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { 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 { if err != nil {
return err return err
} }
if output != "" { if output != "" {
_, _ = fmt.Fprintln(dockerCLI.Out(), output) fmt.Fprintln(dockerCli.Out(), output)
} }
_, _ = fmt.Fprintln(dockerCLI.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
return nil return nil
}, },
Annotations: map[string]string{"version": "1.39"}, Annotations: map[string]string{"version": "1.39"},
ValidArgsFunction: cobra.NoFileCompletions,
DisableFlagsInUseLine: true,
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVarP(&options.force, "force", "f", false, "Do not prompt for confirmation") 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.BoolVarP(&options.all, "all", "a", false, "Remove all unused images, not just dangling ones")
flags.Var(&options.filter, "filter", `Provide filter values (e.g. "until=24h")`) flags.Var(&options.filter, "filter", "Provide filter values (e.g. 'unused-for=24h')")
flags.Var(&options.reservedSpace, "keep-storage", "Amount of disk space to keep for cache") flags.Var(&options.keepStorage, "keep-storage", "Amount of disk space to keep for cache")
return cmd return cmd
} }
@@ -68,32 +56,27 @@ const (
allCacheWarning = `WARNING! This will remove all build cache. Are you sure you want to continue?` 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 := command.PruneFilters(dockerCli, options.filter.Value()) pruneFilters := options.filter.Value()
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
warning := normalWarning warning := normalWarning
if options.all { if options.all {
warning = allCacheWarning warning = allCacheWarning
} }
if !options.force { if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
r, err := prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), warning) return 0, "", nil
if err != nil {
return 0, "", err
}
if !r {
return 0, "", cancelledErr{errors.New("builder prune has been cancelled")}
}
} }
resp, err := dockerCli.Client().BuildCachePrune(ctx, client.BuildCachePruneOptions{ report, err := dockerCli.Client().BuildCachePrune(context.Background(), types.BuildCachePruneOptions{
All: options.all, All: options.all,
ReservedSpace: options.reservedSpace.Value(), KeepStorage: options.keepStorage.Value(),
Filters: pruneFilters, Filters: pruneFilters,
}) })
if err != nil { if err != nil {
return 0, "", err return 0, "", err
} }
report := resp.Report
if len(report.CachesDeleted) > 0 { if len(report.CachesDeleted) > 0 {
var sb strings.Builder var sb strings.Builder
sb.WriteString("Deleted build cache objects:\n") sb.WriteString("Deleted build cache objects:\n")
@@ -107,26 +90,7 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
return report.SpaceReclaimed, output, nil return report.SpaceReclaimed, output, nil
} }
type cancelledErr struct{ error } // CachePrune executes a prune command for build cache
func CachePrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
func (cancelledErr) Cancelled() {} return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter})
// pruneFn prunes the build cache for use in "docker system prune" and
// returns the amount of space reclaimed and a detailed output string.
func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) {
if !options.Confirmed {
// Dry-run: perform validation and produce confirmation before pruning.
var confirmMsg string
if options.All {
confirmMsg = "all build cache"
} else {
confirmMsg = "unused build cache"
}
return 0, confirmMsg, cancelledErr{errors.New("builder prune has been cancelled")}
}
return runPrune(ctx, dockerCLI, pruneOptions{
force: true,
all: options.All,
filter: options.Filter,
})
} }
-26
View File
@@ -1,26 +0,0 @@
package builder
import (
"context"
"errors"
"io"
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/client"
)
func TestBuilderPromptTermination(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
builderPruneFunc: func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
return client.BuildCachePruneResult{}, errors.New("fakeClient builderPruneFunc should not be called")
},
})
cmd := newPruneCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
test.TerminatePrompt(ctx, t, cmd, cli)
}
+70
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
}
+78
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"
]`))
}
+11 -10
View File
@@ -3,33 +3,34 @@ package checkpoint
import ( import (
"context" "context"
"github.com/moby/moby/client" "github.com/docker/docker/api/types"
"github.com/docker/docker/client"
) )
type fakeClient struct { type fakeClient struct {
client.Client client.Client
checkpointCreateFunc func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) checkpointCreateFunc func(container string, options types.CheckpointCreateOptions) error
checkpointDeleteFunc func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) checkpointDeleteFunc func(container string, options types.CheckpointDeleteOptions) error
checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) checkpointListFunc func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error)
} }
func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) { func (cli *fakeClient) CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error {
if cli.checkpointCreateFunc != nil { if cli.checkpointCreateFunc != nil {
return cli.checkpointCreateFunc(container, options) return cli.checkpointCreateFunc(container, options)
} }
return client.CheckpointCreateResult{}, nil return nil
} }
func (cli *fakeClient) CheckpointRemove(_ context.Context, container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) { func (cli *fakeClient) CheckpointDelete(ctx context.Context, container string, options types.CheckpointDeleteOptions) error {
if cli.checkpointDeleteFunc != nil { if cli.checkpointDeleteFunc != nil {
return cli.checkpointDeleteFunc(container, options) return cli.checkpointDeleteFunc(container, options)
} }
return client.CheckpointRemoveResult{}, nil return nil
} }
func (cli *fakeClient) CheckpointList(_ context.Context, container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) { func (cli *fakeClient) CheckpointList(ctx context.Context, container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
if cli.checkpointListFunc != nil { if cli.checkpointListFunc != nil {
return cli.checkpointListFunc(container, options) return cli.checkpointListFunc(container, options)
} }
return client.CheckpointListResult{}, nil return []types.Checkpoint{}, nil
} }
+6 -12
View File
@@ -3,32 +3,26 @@ package checkpoint
import ( import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/commands"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func init() { // NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
commands.Register(newCheckpointCommand) func NewCheckpointCommand(dockerCli command.Cli) *cobra.Command {
}
// newCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
func newCheckpointCommand(dockerCLI command.Cli) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "checkpoint", Use: "checkpoint",
Short: "Manage checkpoints", Short: "Manage checkpoints",
Args: cli.NoArgs, Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCLI.Err()), RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{ Annotations: map[string]string{
"experimental": "", "experimental": "",
"ostype": "linux", "ostype": "linux",
"version": "1.25", "version": "1.25",
}, },
DisableFlagsInUseLine: true,
} }
cmd.AddCommand( cmd.AddCommand(
newCreateCommand(dockerCLI), newCreateCommand(dockerCli),
newListCommand(dockerCLI), newListCommand(dockerCli),
newRemoveCommand(dockerCLI), newRemoveCommand(dockerCli),
) )
return cmd return cmd
} }
+12 -10
View File
@@ -6,7 +6,7 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/moby/moby/client" "github.com/docker/docker/api/types"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -17,7 +17,7 @@ type createOptions struct {
leaveRunning bool leaveRunning bool
} }
func newCreateCommand(dockerCLI command.Cli) *cobra.Command { func newCreateCommand(dockerCli command.Cli) *cobra.Command {
var opts createOptions var opts createOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -27,29 +27,31 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.container = args[0] opts.container = args[0]
opts.checkpoint = args[1] opts.checkpoint = args[1]
return runCreate(cmd.Context(), dockerCLI, opts) return runCreate(dockerCli, opts)
}, },
ValidArgsFunction: cobra.NoFileCompletions,
DisableFlagsInUseLine: true,
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVar(&opts.leaveRunning, "leave-running", false, "Leave the container running after checkpoint") 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 return cmd
} }
func runCreate(ctx context.Context, dockerCLI command.Cli, opts createOptions) error { func runCreate(dockerCli command.Cli, opts createOptions) error {
_, err := dockerCLI.Client().CheckpointCreate(ctx, opts.container, client.CheckpointCreateOptions{ client := dockerCli.Client()
checkpointOpts := types.CheckpointCreateOptions{
CheckpointID: opts.checkpoint, CheckpointID: opts.checkpoint,
CheckpointDir: opts.checkpointDir, CheckpointDir: opts.checkpointDir,
Exit: !opts.leaveRunning, Exit: !opts.leaveRunning,
}) }
err := client.CheckpointCreate(context.Background(), opts.container, checkpointOpts)
if err != nil { if err != nil {
return err return err
} }
_, _ = fmt.Fprintln(dockerCLI.Out(), opts.checkpoint) fmt.Fprintf(dockerCli.Out(), "%s\n", opts.checkpoint)
return nil return nil
} }
+33 -48
View File
@@ -1,36 +1,35 @@
package checkpoint package checkpoint
import ( import (
"errors" "io/ioutil"
"io"
"strconv"
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/moby/moby/client" "github.com/docker/docker/api/types"
"gotest.tools/v3/assert" "github.com/pkg/errors"
is "gotest.tools/v3/assert/cmp" "gotest.tools/assert"
is "gotest.tools/assert/cmp"
) )
func TestCheckpointCreateErrors(t *testing.T) { func TestCheckpointCreateErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
checkpointCreateFunc func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) checkpointCreateFunc func(container string, options types.CheckpointCreateOptions) error
expectedError string expectedError string
}{ }{
{ {
args: []string{"too-few-arguments"}, args: []string{"too-few-arguments"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {
args: []string{"too", "many", "arguments"}, args: []string{"too", "many", "arguments"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},
checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) { checkpointCreateFunc: func(container string, options types.CheckpointCreateOptions) error {
return client.CheckpointCreateResult{}, errors.New("error creating checkpoint for container foo") return errors.Errorf("error creating checkpoint for container foo")
}, },
expectedError: "error creating checkpoint for container foo", expectedError: "error creating checkpoint for container foo",
}, },
@@ -42,46 +41,32 @@ func TestCheckpointCreateErrors(t *testing.T) {
}) })
cmd := newCreateCommand(cli) cmd := newCreateCommand(cli)
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard) cmd.SetOutput(ioutil.Discard)
cmd.SetErr(io.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError) assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
} }
} }
func TestCheckpointCreateWithOptions(t *testing.T) { func TestCheckpointCreateWithOptions(t *testing.T) {
const ( var containerID, checkpointID, checkpointDir string
containerName = "container-foo" var exit bool
checkpointName = "checkpoint-bar" cli := test.NewFakeCli(&fakeClient{
checkpointDir = "/dir/foo" checkpointCreateFunc: func(container string, options types.CheckpointCreateOptions) error {
) containerID = container
checkpointID = options.CheckpointID
for _, tc := range []bool{true, false} { checkpointDir = options.CheckpointDir
leaveRunning := strconv.FormatBool(tc) exit = options.Exit
t.Run("leave-running="+leaveRunning, func(t *testing.T) { return nil
var actualContainerName string },
var actualOptions client.CheckpointCreateOptions })
cli := test.NewFakeCli(&fakeClient{ cmd := newCreateCommand(cli)
checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) { checkpoint := "checkpoint-bar"
actualContainerName = container cmd.SetArgs([]string{"container-foo", checkpoint})
actualOptions = options cmd.Flags().Set("leave-running", "true")
return client.CheckpointCreateResult{}, nil cmd.Flags().Set("checkpoint-dir", "/dir/foo")
}, assert.NilError(t, cmd.Execute())
}) assert.Check(t, is.Equal("container-foo", containerID))
cmd := newCreateCommand(cli) assert.Check(t, is.Equal(checkpoint, checkpointID))
cmd.SetOut(io.Discard) assert.Check(t, is.Equal("/dir/foo", checkpointDir))
cmd.SetErr(io.Discard) assert.Check(t, is.Equal(false, exit))
cmd.SetArgs([]string{containerName, checkpointName}) assert.Check(t, is.Equal(checkpoint, strings.TrimSpace(cli.OutBuffer().String())))
assert.Check(t, cmd.Flags().Set("leave-running", leaveRunning))
assert.Check(t, cmd.Flags().Set("checkpoint-dir", checkpointDir))
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Equal(actualContainerName, containerName))
expected := client.CheckpointCreateOptions{
CheckpointID: checkpointName,
CheckpointDir: checkpointDir,
Exit: !tc,
}
assert.Check(t, is.Equal(actualOptions, expected))
assert.Check(t, is.Equal(strings.TrimSpace(cli.OutBuffer().String()), checkpointName))
})
}
} }
+23 -19
View File
@@ -2,44 +2,48 @@ package checkpoint
import ( import (
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/checkpoint" "github.com/docker/docker/api/types"
) )
const ( const (
defaultCheckpointFormat = "table {{.Name}}" defaultCheckpointFormat = "table {{.Name}}"
checkpointNameHeader = "CHECKPOINT NAME"
checkpointNameHeader = "CHECKPOINT NAME"
) )
// newFormat returns a format for use with a checkpointContext. // NewFormat returns a format for use with a checkpoint Context
func newFormat(source string) formatter.Format { func NewFormat(source string) formatter.Format {
if source == formatter.TableFormatKey { switch source {
case formatter.TableFormatKey:
return defaultCheckpointFormat return defaultCheckpointFormat
} }
return formatter.Format(source) return formatter.Format(source)
} }
// formatWrite writes formatted checkpoints using the Context // FormatWrite writes formatted checkpoints using the Context
func formatWrite(fmtCtx formatter.Context, checkpoints []checkpoint.Summary) error { func FormatWrite(ctx formatter.Context, checkpoints []types.Checkpoint) error {
cpContext := &checkpointContext{ render := func(format func(subContext formatter.SubContext) error) error {
HeaderContext: formatter.HeaderContext{ for _, checkpoint := range checkpoints {
Header: formatter.SubHeaderContext{ if err := format(&checkpointContext{c: checkpoint}); err != nil {
"Name": checkpointNameHeader,
},
},
}
return fmtCtx.Write(cpContext, func(format func(subContext formatter.SubContext) error) error {
for _, cp := range checkpoints {
if err := format(&checkpointContext{c: cp}); err != nil {
return err return err
} }
} }
return nil return nil
}) }
return ctx.Write(newCheckpointContext(), render)
} }
type checkpointContext struct { type checkpointContext struct {
formatter.HeaderContext formatter.HeaderContext
c checkpoint.Summary c types.Checkpoint
}
func newCheckpointContext() *checkpointContext {
cpCtx := checkpointContext{}
cpCtx.Header = formatter.SubHeaderContext{
"Name": checkpointNameHeader,
}
return &cpCtx
} }
func (c *checkpointContext) MarshalJSON() ([]byte, error) { func (c *checkpointContext) MarshalJSON() ([]byte, error) {
+11 -10
View File
@@ -5,8 +5,8 @@ import (
"testing" "testing"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/checkpoint" "github.com/docker/docker/api/types"
"gotest.tools/v3/assert" "gotest.tools/assert"
) )
func TestCheckpointContextFormatWrite(t *testing.T) { func TestCheckpointContextFormatWrite(t *testing.T) {
@@ -15,7 +15,7 @@ func TestCheckpointContextFormatWrite(t *testing.T) {
expected string expected string
}{ }{
{ {
formatter.Context{Format: newFormat(defaultCheckpointFormat)}, formatter.Context{Format: NewFormat(defaultCheckpointFormat)},
`CHECKPOINT NAME `CHECKPOINT NAME
checkpoint-1 checkpoint-1
checkpoint-2 checkpoint-2
@@ -23,14 +23,14 @@ checkpoint-3
`, `,
}, },
{ {
formatter.Context{Format: newFormat("{{.Name}}")}, formatter.Context{Format: NewFormat("{{.Name}}")},
`checkpoint-1 `checkpoint-1
checkpoint-2 checkpoint-2
checkpoint-3 checkpoint-3
`, `,
}, },
{ {
formatter.Context{Format: newFormat("{{.Name}}:")}, formatter.Context{Format: NewFormat("{{.Name}}:")},
`checkpoint-1: `checkpoint-1:
checkpoint-2: checkpoint-2:
checkpoint-3: checkpoint-3:
@@ -38,14 +38,15 @@ checkpoint-3:
}, },
} }
checkpoints := []types.Checkpoint{
{Name: "checkpoint-1"},
{Name: "checkpoint-2"},
{Name: "checkpoint-3"},
}
for _, testcase := range cases { for _, testcase := range cases {
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
testcase.context.Output = out testcase.context.Output = out
err := formatWrite(testcase.context, []checkpoint.Summary{ err := FormatWrite(testcase.context, checkpoints)
{Name: "checkpoint-1"},
{Name: "checkpoint-2"},
{Name: "checkpoint-3"},
})
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, out.String(), testcase.expected) assert.Equal(t, out.String(), testcase.expected)
} }
+15 -13
View File
@@ -5,9 +5,8 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/client" "github.com/docker/docker/api/types"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -15,7 +14,7 @@ type listOptions struct {
checkpointDir string checkpointDir string
} }
func newListCommand(dockerCLI command.Cli) *cobra.Command { func newListCommand(dockerCli command.Cli) *cobra.Command {
var opts listOptions var opts listOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -24,29 +23,32 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command {
Short: "List checkpoints for a container", Short: "List checkpoints for a container",
Args: cli.ExactArgs(1), Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runList(cmd.Context(), dockerCLI, args[0], opts) return runList(dockerCli, args[0], opts)
}, },
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
DisableFlagsInUseLine: true,
} }
flags := cmd.Flags() 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 return cmd
} }
func runList(ctx context.Context, dockerCLI command.Cli, container string, opts listOptions) error { func runList(dockerCli command.Cli, container string, opts listOptions) error {
checkpoints, err := dockerCLI.Client().CheckpointList(ctx, container, client.CheckpointListOptions{ client := dockerCli.Client()
listOpts := types.CheckpointListOptions{
CheckpointDir: opts.checkpointDir, CheckpointDir: opts.checkpointDir,
}) }
checkpoints, err := client.CheckpointList(context.Background(), container, listOpts)
if err != nil { if err != nil {
return err return err
} }
cpCtx := formatter.Context{ cpCtx := formatter.Context{
Output: dockerCLI.Out(), Output: dockerCli.Out(),
Format: newFormat(formatter.TableFormatKey), Format: NewFormat(formatter.TableFormatKey),
} }
return formatWrite(cpCtx, checkpoints.Items) return FormatWrite(cpCtx, checkpoints)
} }
+16 -20
View File
@@ -1,36 +1,35 @@
package checkpoint package checkpoint
import ( import (
"errors" "io/ioutil"
"io"
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/checkpoint" "github.com/docker/docker/api/types"
"github.com/moby/moby/client" "github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/assert/cmp"
"gotest.tools/v3/golden" "gotest.tools/golden"
) )
func TestCheckpointListErrors(t *testing.T) { func TestCheckpointListErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) checkpointListFunc func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error)
expectedError string expectedError string
}{ }{
{ {
args: []string{}, args: []string{},
expectedError: "requires 1 argument", expectedError: "requires exactly 1 argument",
}, },
{ {
args: []string{"too", "many", "arguments"}, args: []string{"too", "many", "arguments"},
expectedError: "requires 1 argument", expectedError: "requires exactly 1 argument",
}, },
{ {
args: []string{"foo"}, args: []string{"foo"},
checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) { checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
return client.CheckpointListResult{}, errors.New("error getting checkpoints for container foo") return []types.Checkpoint{}, errors.Errorf("error getting checkpoints for container foo")
}, },
expectedError: "error getting checkpoints for container foo", expectedError: "error getting checkpoints for container foo",
}, },
@@ -42,8 +41,7 @@ func TestCheckpointListErrors(t *testing.T) {
}) })
cmd := newListCommand(cli) cmd := newListCommand(cli)
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard) cmd.SetOutput(ioutil.Discard)
cmd.SetErr(io.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError) assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
} }
} }
@@ -51,19 +49,17 @@ func TestCheckpointListErrors(t *testing.T) {
func TestCheckpointListWithOptions(t *testing.T) { func TestCheckpointListWithOptions(t *testing.T) {
var containerID, checkpointDir string var containerID, checkpointDir string
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) { checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
containerID = container containerID = container
checkpointDir = options.CheckpointDir checkpointDir = options.CheckpointDir
return client.CheckpointListResult{ return []types.Checkpoint{
Items: []checkpoint.Summary{ {Name: "checkpoint-foo"},
{Name: "checkpoint-foo"},
},
}, nil }, nil
}, },
}) })
cmd := newListCommand(cli) cmd := newListCommand(cli)
cmd.SetArgs([]string{"container-foo"}) cmd.SetArgs([]string{"container-foo"})
assert.Check(t, cmd.Flags().Set("checkpoint-dir", "/dir/foo")) cmd.Flags().Set("checkpoint-dir", "/dir/foo")
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
assert.Check(t, is.Equal("container-foo", containerID)) assert.Check(t, is.Equal("container-foo", containerID))
assert.Check(t, is.Equal("/dir/foo", checkpointDir)) assert.Check(t, is.Equal("/dir/foo", checkpointDir))
+17 -10
View File
@@ -1,9 +1,11 @@
package checkpoint package checkpoint
import ( import (
"context"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/moby/moby/client" "github.com/docker/docker/api/types"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -11,7 +13,7 @@ type removeOptions struct {
checkpointDir string checkpointDir string
} }
func newRemoveCommand(dockerCLI command.Cli) *cobra.Command { func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
var opts removeOptions var opts removeOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -20,18 +22,23 @@ func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
Short: "Remove a checkpoint", Short: "Remove a checkpoint",
Args: cli.ExactArgs(2), Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
containerID, checkpointID := args[0], args[1] return runRemove(dockerCli, args[0], args[1], opts)
_, err := dockerCLI.Client().CheckpointRemove(cmd.Context(), containerID, client.CheckpointRemoveOptions{
CheckpointID: checkpointID,
CheckpointDir: opts.checkpointDir,
})
return err
}, },
DisableFlagsInUseLine: true,
} }
flags := cmd.Flags() 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 return cmd
} }
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)
}
+14 -15
View File
@@ -1,34 +1,34 @@
package checkpoint package checkpoint
import ( import (
"errors" "io/ioutil"
"io"
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/moby/moby/client" "github.com/docker/docker/api/types"
"gotest.tools/v3/assert" "github.com/pkg/errors"
is "gotest.tools/v3/assert/cmp" "gotest.tools/assert"
is "gotest.tools/assert/cmp"
) )
func TestCheckpointRemoveErrors(t *testing.T) { func TestCheckpointRemoveErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
checkpointDeleteFunc func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) checkpointDeleteFunc func(container string, options types.CheckpointDeleteOptions) error
expectedError string expectedError string
}{ }{
{ {
args: []string{"too-few-arguments"}, args: []string{"too-few-arguments"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {
args: []string{"too", "many", "arguments"}, args: []string{"too", "many", "arguments"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},
checkpointDeleteFunc: func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) { checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error {
return client.CheckpointRemoveResult{}, errors.New("error deleting checkpoint") return errors.Errorf("error deleting checkpoint")
}, },
expectedError: "error deleting checkpoint", expectedError: "error deleting checkpoint",
}, },
@@ -40,8 +40,7 @@ func TestCheckpointRemoveErrors(t *testing.T) {
}) })
cmd := newRemoveCommand(cli) cmd := newRemoveCommand(cli)
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard) cmd.SetOutput(ioutil.Discard)
cmd.SetErr(io.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError) assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
} }
} }
@@ -49,16 +48,16 @@ func TestCheckpointRemoveErrors(t *testing.T) {
func TestCheckpointRemoveWithOptions(t *testing.T) { func TestCheckpointRemoveWithOptions(t *testing.T) {
var containerID, checkpointID, checkpointDir string var containerID, checkpointID, checkpointDir string
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
checkpointDeleteFunc: func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) { checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error {
containerID = container containerID = container
checkpointID = options.CheckpointID checkpointID = options.CheckpointID
checkpointDir = options.CheckpointDir checkpointDir = options.CheckpointDir
return client.CheckpointRemoveResult{}, nil return nil
}, },
}) })
cmd := newRemoveCommand(cli) cmd := newRemoveCommand(cli)
cmd.SetArgs([]string{"container-foo", "checkpoint-bar"}) cmd.SetArgs([]string{"container-foo", "checkpoint-bar"})
assert.Check(t, cmd.Flags().Set("checkpoint-dir", "/dir/foo")) cmd.Flags().Set("checkpoint-dir", "/dir/foo")
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
assert.Check(t, is.Equal("container-foo", containerID)) assert.Check(t, is.Equal("container-foo", containerID))
assert.Check(t, is.Equal("checkpoint-bar", checkpointID)) assert.Check(t, is.Equal("checkpoint-bar", checkpointID))
+292 -369
View File
@@ -1,104 +1,97 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package command package command
import ( import (
"context" "context"
"errors"
"fmt"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"sync"
"time"
"github.com/docker/cli/cli/config" "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/config/configfile"
dcontext "github.com/docker/cli/cli/context" dcontext "github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/docker" "github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/store" "github.com/docker/cli/cli/context/store"
"github.com/docker/cli/cli/debug" "github.com/docker/cli/cli/debug"
cliflags "github.com/docker/cli/cli/flags" cliflags "github.com/docker/cli/cli/flags"
manifeststore "github.com/docker/cli/cli/manifest/store"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/cli/version" "github.com/docker/cli/cli/version"
"github.com/docker/cli/internal/containerizedengine"
dopts "github.com/docker/cli/opts" dopts "github.com/docker/cli/opts"
"github.com/moby/moby/api/types/build" clitypes "github.com/docker/cli/types"
"github.com/moby/moby/client" "github.com/docker/docker/api/types"
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/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 // Streams is an interface which exposes the standard input and output streams
type Streams interface { type Streams interface {
In() *streams.In In() *streams.In
Out() *streams.Out Out() *streams.Out
Err() *streams.Out Err() io.Writer
} }
// Cli represents the docker command line client. // Cli represents the docker command line client.
type Cli interface { type Cli interface {
Client() client.APIClient Client() client.APIClient
Streams Out() *streams.Out
Err() io.Writer
In() *streams.In
SetIn(in *streams.In) SetIn(in *streams.In)
config.Provider Apply(ops ...DockerCliOption) error
ConfigFile() *configfile.ConfigFile
ServerInfo() ServerInfo ServerInfo() ServerInfo
CurrentVersion() string ClientInfo() ClientInfo
BuildKitEnabled() (bool, error) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
DefaultVersion() string
ManifestStore() manifeststore.Store
RegistryClient(bool) registryclient.RegistryClient
ContentTrustEnabled() bool
NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error)
ContextStore() store.Store ContextStore() store.Store
CurrentContext() string CurrentContext() string
StackOrchestrator(flagValue string) (Orchestrator, error)
DockerEndpoint() docker.Endpoint DockerEndpoint() docker.Endpoint
TelemetryClient
} }
// DockerCli is an instance the docker command line client. // DockerCli is an instance the docker command line client.
// Instances of the client should be created using the [NewDockerCli] // Instances of the client can be returned from NewDockerCli.
// constructor to make sure they are properly initialized with defaults
// set.
type DockerCli struct { type DockerCli struct {
configFile *configfile.ConfigFile configFile *configfile.ConfigFile
options *cliflags.ClientOptions in *streams.In
clientOpts []client.Opt out *streams.Out
in *streams.In err io.Writer
out *streams.Out client client.APIClient
err *streams.Out serverInfo ServerInfo
client client.APIClient clientInfo ClientInfo
serverInfo ServerInfo contentTrust bool
contextStore store.Store newContainerizeClient func(string) (clitypes.ContainerizedClient, error)
currentContext string contextStore store.Store
init sync.Once currentContext string
initErr error dockerEndpoint docker.Endpoint
dockerEndpoint docker.Endpoint contextStoreConfig store.Config
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
} }
// CurrentVersion returns the API version currently negotiated, or the default // DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
// version otherwise. func (cli *DockerCli) DefaultVersion() string {
func (cli *DockerCli) CurrentVersion() string { return cli.clientInfo.DefaultVersion
_ = cli.initialize()
if cli.client == nil {
return client.MaxAPIVersion
}
return cli.client.ClientVersion()
} }
// Client returns the APIClient // Client returns the APIClient
func (cli *DockerCli) Client() client.APIClient { func (cli *DockerCli) Client() client.APIClient {
if err := cli.initialize(); err != nil {
_, _ = fmt.Fprintln(cli.Err(), "Failed to initialize:", err)
os.Exit(1)
}
return cli.client return cli.client
} }
@@ -108,7 +101,7 @@ func (cli *DockerCli) Out() *streams.Out {
} }
// Err returns the writer used for stderr // Err returns the writer used for stderr
func (cli *DockerCli) Err() *streams.Out { func (cli *DockerCli) Err() io.Writer {
return cli.err return cli.err
} }
@@ -125,7 +118,7 @@ func (cli *DockerCli) In() *streams.In {
// ShowHelp shows the command help. // ShowHelp shows the command help.
func ShowHelp(err io.Writer) func(*cobra.Command, []string) error { func ShowHelp(err io.Writer) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error {
cmd.SetOut(err) cmd.SetOutput(err)
cmd.HelpFunc()(cmd, args) cmd.HelpFunc()(cmd, args)
return nil return nil
} }
@@ -133,200 +126,171 @@ func ShowHelp(err io.Writer) func(*cobra.Command, []string) error {
// ConfigFile returns the ConfigFile // ConfigFile returns the ConfigFile
func (cli *DockerCli) ConfigFile() *configfile.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 return cli.configFile
} }
// ServerInfo returns the server version details for the host this client is // ServerInfo returns the server version details for the host this client is
// connected to // connected to
func (cli *DockerCli) ServerInfo() ServerInfo { func (cli *DockerCli) ServerInfo() ServerInfo {
_ = cli.initialize()
return cli.serverInfo return cli.serverInfo
} }
// BuildKitEnabled returns buildkit is enabled or not. // ClientInfo returns the client details for the cli
func (cli *DockerCli) BuildKitEnabled() (bool, error) { func (cli *DockerCli) ClientInfo() ClientInfo {
// use DOCKER_BUILDKIT env var value if set and not empty return cli.clientInfo
if v := os.Getenv("DOCKER_BUILDKIT"); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false, fmt.Errorf("DOCKER_BUILDKIT environment variable expects boolean value: %w", err)
}
return 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 == build.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. // ContentTrustEnabled returns whether content trust has been enabled by an
func (cli *DockerCli) HooksEnabled() bool { // environment variable.
// use DOCKER_CLI_HOOKS env var value if set and not empty func (cli *DockerCli) ContentTrustEnabled() bool {
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" { return cli.contentTrust
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 { if err != nil {
return false return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
} }
return enabled
} }
// legacy support DOCKER_CLI_HINTS env var return buildkitEnabled, nil
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" { }
enabled, err := strconv.ParseBool(v)
if err != nil { // ManifestStore returns a store for local manifests
return false func (cli *DockerCli) ManifestStore() manifeststore.Store {
} // TODO: support override default location from config file
return enabled return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
}
// RegistryClient returns a client for communicating with a Docker distribution
// registry
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
resolver := func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
return ResolveAuthConfig(ctx, cli, index)
} }
featuresMap := cli.ConfigFile().Features return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
if v, ok := featuresMap["hooks"]; ok { }
enabled, err := strconv.ParseBool(v)
if err != nil { // InitializeOpt is the type of the functional options passed to DockerCli.Initialize
return false type InitializeOpt func(dockerCli *DockerCli) error
}
return enabled // 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)) InitializeOpt {
return func(dockerCli *DockerCli) error {
var err error
dockerCli.client, err = makeClient(dockerCli)
return err
} }
// default to false
return false
} }
// Initialize the dockerCli runs initialization that must happen after command // Initialize the dockerCli runs initialization that must happen after command
// line flags are parsed. // line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error { func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...InitializeOpt) error {
var err error
for _, o := range ops { for _, o := range ops {
if err := o(cli); err != nil { if err := o(cli); err != nil {
return err return err
} }
} }
cliflags.SetLogLevel(opts.LogLevel) cliflags.SetLogLevel(opts.Common.LogLevel)
if opts.ConfigDir != "" { if opts.ConfigDir != "" {
config.SetDir(opts.ConfigDir) cliconfig.SetDir(opts.ConfigDir)
} }
if opts.Debug { if opts.Common.Debug {
debug.Enable() debug.Enable()
} }
if opts.Context != "" && len(opts.Hosts) > 0 {
return errors.New("conflicting options: cannot specify both --host and --context") cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
baseContextStore := store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig)
cli.contextStore = &ContextStoreWithDefault{
Store: baseContextStore,
Resolver: func() (*DefaultContext, error) {
return ResolveDefaultContext(opts.Common, cli.ConfigFile(), cli.contextStoreConfig, cli.Err())
},
}
cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore)
if err != nil {
return err
}
cli.dockerEndpoint, err = resolveDockerEndpoint(cli.contextStore, cli.currentContext)
if err != nil {
return errors.Wrap(err, "unable to resolve docker endpoint")
} }
if cli.contextStoreConfig == nil { if cli.client == nil {
// This path can be hit when calling Initialize on a DockerCli that's cli.client, err = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile)
// not constructed through [NewDockerCli]. Using the default context if tlsconfig.IsErrEncryptedKey(err) {
// store without a config set will result in Endpoints from contexts passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
// not being type-mapped correctly, and used as a generic "map[string]any", newClient := func(password string) (client.APIClient, error) {
// instead of a [docker.EndpointMeta]. cli.dockerEndpoint.TLSPassword = password
// return newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile)
// When looking up the API endpoint (using [EndpointFromContext]), no }
// endpoint will be found, and a default, empty endpoint will be used cli.client, err = getClientWithPassword(passRetriever, newClient)
// instead which in its turn, causes newAPIClientFromEndpoint to }
// be initialized with the default config instead of settings for if err != nil {
// the current context (which may mean; connecting with the wrong
// endpoint and/or TLS Config to be missing).
//
// [EndpointFromContext]: https://github.com/docker/cli/blob/33494921b80fd0b5a06acc3a34fa288de4bb2e6b/cli/context/docker/load.go#L139-L149
if err := WithDefaultContextStoreConfig()(cli); err != nil {
return err return err
} }
} }
var experimentalValue string
cli.options = opts // Environment variable always overrides configuration
cli.configFile = config.LoadDefaultConfigFile(cli.err) if experimentalValue = os.Getenv("DOCKER_CLI_EXPERIMENTAL"); experimentalValue == "" {
cli.currentContext = resolveContextName(cli.options, cli.configFile) experimentalValue = cli.configFile.Experimental
cli.contextStore = &ContextStoreWithDefault{
Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig),
Resolver: func() (*DefaultContext, error) {
return resolveDefaultContext(cli.options, *cli.contextStoreConfig)
},
} }
hasExperimental, err := isEnabled(experimentalValue)
// TODO(krissetto): pass ctx to the funcs instead of using this if err != nil {
if cli.enableGlobalMeter { return errors.Wrap(err, "Experimental field")
cli.createGlobalMeterProvider(cli.baseCtx)
} }
if cli.enableGlobalTracer { cli.clientInfo = ClientInfo{
cli.createGlobalTracerProvider(cli.baseCtx) DefaultVersion: cli.client.ClientVersion(),
HasExperimental: hasExperimental,
} }
filterResourceAttributesEnvvar() cli.initializeFromClient()
// early return if GODEBUG is already set or the docker context is
// the default context, i.e. is a virtual context where we won't override
// any GODEBUG values.
if v := os.Getenv("GODEBUG"); cli.currentContext == DefaultContextName || v != "" {
return nil
}
meta, err := cli.contextStore.GetMetadata(cli.currentContext)
if err == nil {
setGoDebug(meta)
}
return nil return nil
} }
// NewAPIClientFromFlags creates a new APIClient from command line flags // NewAPIClientFromFlags creates a new APIClient from command line flags
func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.ConfigFile) (client.APIClient, error) { func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
if opts.Context != "" && len(opts.Hosts) > 0 {
return nil, errors.New("conflicting options: cannot specify both --host and --context")
}
storeConfig := DefaultContextStoreConfig() storeConfig := DefaultContextStoreConfig()
contextStore := &ContextStoreWithDefault{ store := &ContextStoreWithDefault{
Store: store.New(config.ContextStoreDir(), storeConfig), Store: store.New(cliconfig.ContextStoreDir(), storeConfig),
Resolver: func() (*DefaultContext, error) { 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 { if err != nil {
return nil, fmt.Errorf("unable to resolve docker endpoint: %w", err) return nil, err
} }
return newAPIClientFromEndpoint(endpoint, configFile, client.WithUserAgent(UserAgent())) endpoint, err := resolveDockerEndpoint(store, contextName)
if err != nil {
return nil, errors.Wrap(err, "unable to resolve docker endpoint")
}
return newAPIClientFromEndpoint(endpoint, configFile)
} }
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile, extraOpts ...client.Opt) (client.APIClient, error) { func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
opts, err := ep.ClientOpts() clientOpts, err := ep.ClientOpts()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(configFile.HTTPHeaders) > 0 { customHeaders := configFile.HTTPHeaders
opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders)) if customHeaders == nil {
customHeaders = map[string]string{}
} }
withCustomHeaders, err := withCustomHeadersFromEnv() customHeaders["User-Agent"] = UserAgent()
if err != nil { clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))
return nil, err return client.NewClientWithOpts(clientOpts...)
}
if withCustomHeaders != nil {
opts = append(opts, withCustomHeaders)
}
opts = append(opts, extraOpts...)
apiClient, err := client.New(opts...)
if err != nil {
return nil, err
}
return apiClient, nil
} }
func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) { 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) ctxMeta, err := s.GetMetadata(contextName)
if err != nil { if err != nil {
return docker.Endpoint{}, err return docker.Endpoint{}, err
@@ -339,11 +303,8 @@ func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint,
} }
// Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags) // Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) { func resolveDefaultDockerEndpoint(opts *cliflags.CommonOptions) (docker.Endpoint, error) {
// defaultToTLS determines whether we should use a TLS host as default host, err := getServerHost(opts.Hosts, opts.TLSOptions)
// if nothing was configured by the user.
defaultToTLS := opts.TLSOptions != nil
host, err := getServerHost(opts.Hosts, defaultToTLS)
if err != nil { if err != nil {
return docker.Endpoint{}, err return docker.Endpoint{}, err
} }
@@ -370,32 +331,59 @@ func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint
}, nil }, nil
} }
func (cli *DockerCli) getInitTimeout() time.Duration { func isEnabled(value string) (bool, error) {
if cli.initTimeout != 0 { switch value {
return cli.initTimeout 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() { func (cli *DockerCli) initializeFromClient() {
ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout()) ping, err := cli.client.Ping(context.Background())
defer cancel()
ping, err := cli.client.Ping(ctx, client.PingOptions{
NegotiateAPIVersion: true,
ForceNegotiate: true,
})
if err != nil { if err != nil {
// Default to true if we fail to connect to daemon // Default to true if we fail to connect to daemon
cli.serverInfo = ServerInfo{HasExperimental: true} cli.serverInfo = ServerInfo{HasExperimental: true}
if ping.APIVersion != "" {
cli.client.NegotiateAPIVersionPing(ping)
}
return return
} }
cli.serverInfo = ServerInfo{ cli.serverInfo = ServerInfo{
HasExperimental: ping.Experimental, HasExperimental: ping.Experimental,
OSType: ping.OSType, OSType: ping.OSType,
BuildkitVersion: ping.BuilderVersion, 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 // ContextStore returns the ContextStore
@@ -403,150 +391,43 @@ func (cli *DockerCli) ContextStore() store.Store {
return cli.contextStore return cli.contextStore
} }
// CurrentContext returns the current context name, based on flags, // CurrentContext returns the current context name
// 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.
func (cli *DockerCli) CurrentContext() string { func (cli *DockerCli) CurrentContext() string {
return cli.currentContext return cli.currentContext
} }
// CurrentContext returns the current context name, based on flags, // StackOrchestrator resolves which stack orchestrator is in use
// environment variables and the cli configuration file. It does not func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error) {
// validate if the given context exists or if it's valid; errors may currentContext := cli.CurrentContext()
// occur when trying to use it. ctxRaw, err := cli.ContextStore().GetMetadata(currentContext)
// if store.IsErrContextDoesNotExist(err) {
// Refer to [DockerCli.CurrentContext] above for further details. // case where the currentContext has been removed (CLI behavior is to fallback to using DOCKER_HOST based resolution)
func resolveContextName(opts *cliflags.ClientOptions, cfg *configfile.ConfigFile) string { return GetStackOrchestrator(flagValue, "", cli.ConfigFile().StackOrchestrator, cli.Err())
if opts != nil && opts.Context != "" {
return opts.Context
} }
if opts != nil && len(opts.Hosts) > 0 { if err != nil {
return DefaultContextName return "", err
} }
if os.Getenv(client.EnvOverrideHost) != "" { ctxMeta, err := GetDockerContext(ctxRaw)
return DefaultContextName if err != nil {
return "", err
} }
if ctxName := os.Getenv(EnvOverrideContext); ctxName != "" { ctxOrchestrator := string(ctxMeta.StackOrchestrator)
return ctxName return GetStackOrchestrator(flagValue, ctxOrchestrator, cli.ConfigFile().StackOrchestrator, cli.Err())
}
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
} }
// DockerEndpoint returns the current docker endpoint // DockerEndpoint returns the current docker endpoint
func (cli *DockerCli) DockerEndpoint() 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.Fprintln(cli.Err(), cli.initErr)
}
return cli.dockerEndpoint return cli.dockerEndpoint
} }
func (cli *DockerCli) getDockerEndPoint() (ep docker.Endpoint, err error) { // Apply all the operation on the cli
cn := cli.CurrentContext() func (cli *DockerCli) Apply(ops ...DockerCliOption) error {
if cn == DefaultContextName { for _, op := range ops {
return resolveDefaultDockerEndpoint(cli.options) if err := op(cli); err != nil {
return err
}
} }
return resolveDockerEndpoint(cli.contextStore, cn) return nil
}
// setGoDebug is an escape hatch that sets the GODEBUG environment
// variable value using docker context metadata.
//
// {
// "Name": "my-context",
// "Metadata": { "GODEBUG": "x509negativeserial=1" }
// }
//
// WARNING: Setting x509negativeserial=1 allows Go's x509 library to accept
// X.509 certificates with negative serial numbers.
// This behavior is deprecated and non-compliant with current security
// standards (RFC 5280). Accepting negative serial numbers can introduce
// serious security vulnerabilities, including the risk of certificate
// collision or bypass attacks.
// This option should only be used for legacy compatibility and never in
// production environments.
// Use at your own risk.
func setGoDebug(meta store.Metadata) {
fieldName := "GODEBUG"
godebugEnv := os.Getenv(fieldName)
// early return if GODEBUG is already set. We don't want to override what
// the user already sets.
if godebugEnv != "" {
return
}
var cfg any
var ok bool
switch m := meta.Metadata.(type) {
case DockerContext:
cfg, ok = m.AdditionalFields[fieldName]
if !ok {
return
}
case map[string]any:
cfg, ok = m[fieldName]
if !ok {
return
}
default:
return
}
v, ok := cfg.(string)
if !ok {
return
}
// set the GODEBUG environment variable with whatever was in the context
_ = os.Setenv(fieldName, v)
}
func (cli *DockerCli) initialize() error {
cli.init.Do(func() {
cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint()
if cli.initErr != nil {
cli.initErr = fmt.Errorf("unable to resolve docker endpoint: %w", cli.initErr)
return
}
if cli.client == nil {
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile, cli.clientOpts...); cli.initErr != nil {
return
}
}
if cli.baseCtx == nil {
cli.baseCtx = context.Background()
}
cli.initializeFromClient()
})
return cli.initErr
} }
// ServerInfo stores details about the supported features and platform of the // ServerInfo stores details about the supported features and platform of the
@@ -554,57 +435,99 @@ func (cli *DockerCli) initialize() error {
type ServerInfo struct { type ServerInfo struct {
HasExperimental bool HasExperimental bool
OSType string OSType string
BuildkitVersion build.BuilderVersion BuildkitVersion types.BuilderVersion
}
// SwarmStatus provides information about the current swarm status of the // ClientInfo stores details about the supported features of the client
// engine, obtained from the "Swarm" header in the API response. type ClientInfo struct {
// HasExperimental bool
// It can be a nil struct if the API version does not provide this header DefaultVersion string
// in the ping response, or if an error occurred, in which case the client
// should use other ways to get the current swarm status, such as the /swarm
// endpoint.
SwarmStatus *client.SwarmStatus
} }
// NewDockerCli returns a DockerCli instance with all operators applied on it. // NewDockerCli returns a DockerCli instance with all operators applied on it.
// It applies by default the standard streams, and the content trust from // It applies by default the standard streams, the content trust from
// environment. // environment and the default containerized client constructor operations.
func NewDockerCli(ops ...CLIOption) (*DockerCli, error) { func NewDockerCli(ops ...DockerCliOption) (*DockerCli, error) {
defaultOps := make([]CLIOption, 0, 3+len(ops)) cli := &DockerCli{}
defaultOps = append(defaultOps, defaultOps := []DockerCliOption{
WithDefaultContextStoreConfig(), WithContentTrustFromEnv(),
WithStandardStreams(), WithContainerizedClient(containerizedengine.NewClient),
WithUserAgent(UserAgent()), }
) cli.contextStoreConfig = DefaultContextStoreConfig()
ops = append(defaultOps, ops...) ops = append(defaultOps, ops...)
if err := cli.Apply(ops...); err != nil {
cli := &DockerCli{baseCtx: context.Background()} return nil, err
for _, op := range ops { }
if err := op(cli); err != nil { if cli.out == nil || cli.in == nil || cli.err == nil {
return nil, err 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 return cli, nil
} }
func getServerHost(hosts []string, defaultToTLS bool) (string, error) { func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
var host string
switch len(hosts) { switch len(hosts) {
case 0: case 0:
return dopts.ParseHost(defaultToTLS, os.Getenv(client.EnvOverrideHost)) host = os.Getenv("DOCKER_HOST")
case 1: case 1:
return dopts.ParseHost(defaultToTLS, hosts[0]) host = hosts[0]
default: default:
return "", errors.New("specify only one -H") return "", errors.New("Please specify only one -H")
} }
return dopts.ParseHost(tlsOptions != nil, host)
} }
// UserAgent returns the default user agent string used for making API requests. // UserAgent returns the user agent string used for making API requests
func UserAgent() string { func UserAgent() string {
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")" 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{ 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 // RegisterDefaultStoreEndpoints registers a new named endpoint
@@ -618,7 +541,7 @@ func RegisterDefaultStoreEndpoints(ep ...store.NamedTypeGetter) {
// DefaultContextStoreConfig returns a new store.Config with the default set of endpoints configured. // DefaultContextStoreConfig returns a new store.Config with the default set of endpoints configured.
func DefaultContextStoreConfig() store.Config { func DefaultContextStoreConfig() store.Config {
return store.NewConfig( return store.NewConfig(
func() any { return &DockerContext{} }, func() interface{} { return &DockerContext{} },
defaultStoreEndpoints..., defaultStoreEndpoints...,
) )
} }
+47 -179
View File
@@ -1,58 +1,44 @@
package command package command
import ( import (
"context"
"encoding/csv"
"errors"
"fmt" "fmt"
"io" "io"
"net/http"
"os" "os"
"strings" "strconv"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/store"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/moby/moby/client" clitypes "github.com/docker/cli/types"
"github.com/moby/term" "github.com/docker/docker/pkg/term"
) )
// CLIOption is a functional argument to apply options to a [DockerCli]. These // DockerCliOption applies a modification on a DockerCli.
// options can be passed to [NewDockerCli] to initialize a new CLI, or type DockerCliOption func(cli *DockerCli) error
// applied with [DockerCli.Initialize] or [DockerCli.Apply].
type CLIOption func(cli *DockerCli) error
// WithStandardStreams sets a cli in, out and err streams with the standard streams. // WithStandardStreams sets a cli in, out and err streams with the standard streams.
func WithStandardStreams() CLIOption { func WithStandardStreams() DockerCliOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
// Set terminal emulation based on platform as required. // Set terminal emulation based on platform as required.
stdin, stdout, stderr := term.StdStreams() stdin, stdout, stderr := term.StdStreams()
cli.in = streams.NewIn(stdin) cli.in = streams.NewIn(stdin)
cli.out = streams.NewOut(stdout) cli.out = streams.NewOut(stdout)
cli.err = streams.NewOut(stderr) cli.err = 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
return nil return nil
} }
} }
// WithCombinedStreams uses the same stream for the output and error streams. // 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 { return func(cli *DockerCli) error {
s := streams.NewOut(combined) cli.out = streams.NewOut(combined)
cli.out = s cli.err = combined
cli.err = s
return nil return nil
} }
} }
// WithInputStream sets a cli input stream. // WithInputStream sets a cli input stream.
func WithInputStream(in io.ReadCloser) CLIOption { func WithInputStream(in io.ReadCloser) DockerCliOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cli.in = streams.NewIn(in) cli.in = streams.NewIn(in)
return nil return nil
@@ -60,7 +46,7 @@ func WithInputStream(in io.ReadCloser) CLIOption {
} }
// WithOutputStream sets a cli output stream. // WithOutputStream sets a cli output stream.
func WithOutputStream(out io.Writer) CLIOption { func WithOutputStream(out io.Writer) DockerCliOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cli.out = streams.NewOut(out) cli.out = streams.NewOut(out)
return nil return nil
@@ -68,170 +54,52 @@ func WithOutputStream(out io.Writer) CLIOption {
} }
// WithErrorStream sets a cli error stream. // WithErrorStream sets a cli error stream.
func WithErrorStream(err io.Writer) CLIOption { func WithErrorStream(err io.Writer) DockerCliOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cli.err = streams.NewOut(err) cli.err = err
return nil return nil
} }
} }
// WithDefaultContextStoreConfig configures the cli to use the default context store configuration. // WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
func WithDefaultContextStoreConfig() CLIOption { func WithContentTrustFromEnv() DockerCliOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cfg := DefaultContextStoreConfig() cli.contentTrust = false
cli.contextStoreConfig = &cfg if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" {
return nil if t, err := strconv.ParseBool(e); t || err != nil {
} // treat any other value as true
} cli.contentTrust = true
}
// WithAPIClient configures the cli to use the given API client.
func WithAPIClient(c client.APIClient) CLIOption {
return func(cli *DockerCli) error {
cli.client = c
return nil
}
}
// WithInitializeClient is passed to [DockerCli.Initialize] to initialize
// an API Client for use by the CLI.
func WithInitializeClient(makeClient func(*DockerCli) (client.APIClient, error)) CLIOption {
return func(cli *DockerCli) error {
c, err := makeClient(cli)
if err != nil {
return err
} }
return WithAPIClient(c)(cli)
}
}
// WithAPIClientOptions configures additional [client.Opt] to use when
// initializing the API client. These options have no effect if a custom
// client is set (through [WithAPIClient] or [WithInitializeClient]).
func WithAPIClientOptions(c ...client.Opt) CLIOption {
return func(cli *DockerCli) error {
cli.clientOpts = append(cli.clientOpts, c...)
return nil return nil
} }
} }
// envOverrideHTTPHeaders is the name of the environment-variable that can be // WithContentTrust enables content trust on a cli.
// used to set custom HTTP headers to be sent by the client. This environment func WithContentTrust(enabled bool) DockerCliOption {
// 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, error) {
value := os.Getenv(envOverrideHTTPHeaders)
if value == "" {
return nil, nil
}
csvReader := csv.NewReader(strings.NewReader(value))
fields, err := csvReader.Read()
if err != nil {
return nil, invalidParameter(fmt.Errorf(
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
envOverrideHTTPHeaders,
))
}
if len(fields) == 0 {
return nil, nil
}
env := map[string]string{}
for _, kv := range fields {
k, v, hasValue := strings.Cut(kv, "=")
// Only strip whitespace in keys; preserve whitespace in values.
k = strings.TrimSpace(k)
if k == "" {
return nil, invalidParameter(fmt.Errorf(
`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`,
envOverrideHTTPHeaders, kv,
))
}
// We don't currently allow empty key=value pairs, and produce an error.
// This is something we could allow in future (e.g. to read value
// from an environment variable with the same name). In the meantime,
// produce an error to prevent users from depending on this.
if !hasValue {
return nil, invalidParameter(fmt.Errorf(
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
envOverrideHTTPHeaders, kv,
))
}
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, 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), nil
}
// WithUserAgent configures the User-Agent string for cli HTTP requests.
func WithUserAgent(userAgent string) CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
if userAgent == "" { cli.contentTrust = enabled
return errors.New("user agent cannot be blank") return nil
}
}
// WithContainerizedClient sets the containerized client constructor on a cli.
func WithContainerizedClient(containerizedFn func(string) (clitypes.ContainerizedClient, error)) DockerCliOption {
return func(cli *DockerCli) error {
cli.newContainerizeClient = containerizedFn
return nil
}
}
// 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 {
switch endpointName {
case docker.DockerEndpoint:
return fmt.Errorf("cannot change %q endpoint type", endpointName)
} }
cli.clientOpts = append(cli.clientOpts, client.WithUserAgent(userAgent)) cli.contextStoreConfig.SetEndpoint(endpointName, endpointType)
return nil return nil
} }
} }
+37
View File
@@ -0,0 +1,37 @@
package command
import (
"os"
"testing"
"gotest.tools/assert"
)
func contentTrustEnabled(t *testing.T) bool {
var cli DockerCli
assert.NilError(t, WithContentTrustFromEnv()(&cli))
return cli.contentTrust
}
// NB: Do not t.Parallel() this test -- it messes with the process environment.
func TestWithContentTrustFromEnv(t *testing.T) {
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.Assert(t, !contentTrustEnabled(t))
}
+201 -321
View File
@@ -3,25 +3,26 @@ package command
import ( import (
"bytes" "bytes"
"context" "context"
"errors" "crypto/x509"
"fmt" "fmt"
"io" "io/ioutil"
"net"
"net/http" "net/http"
"net/http/httptest"
"os" "os"
"path/filepath"
"runtime" "runtime"
"strings"
"testing" "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/config/configfile"
"github.com/docker/cli/cli/context/store"
"github.com/docker/cli/cli/flags" "github.com/docker/cli/cli/flags"
"github.com/moby/moby/client" clitypes "github.com/docker/cli/types"
"gotest.tools/v3/assert" "github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/env"
"gotest.tools/fs"
) )
func TestNewAPIClientFromFlags(t *testing.T) { func TestNewAPIClientFromFlags(t *testing.T) {
@@ -29,289 +30,287 @@ func TestNewAPIClientFromFlags(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
host = "npipe://./" host = "npipe://./"
} }
opts := &flags.ClientOptions{Hosts: []string{host}} opts := &flags.CommonOptions{Hosts: []string{host}}
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host)
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
}
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(), client.MaxAPIVersion)
}
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}}
configFile := &configfile.ConfigFile{ configFile := &configfile.ConfigFile{
HTTPHeaders: map[string]string{ HTTPHeaders: map[string]string{
"My-Header": "Custom-Value", "My-Header": "Custom-Value",
}, },
} }
apiclient, err := NewAPIClientFromFlags(opts, configFile)
apiClient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host) assert.Check(t, is.Equal(host, apiclient.DaemonHost()))
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
// 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"})
expectedHeaders := map[string]string{ expectedHeaders := map[string]string{
"My-Header": "Custom-Value", "My-Header": "Custom-Value",
"User-Agent": UserAgent(), "User-Agent": UserAgent(),
} }
_, err = apiClient.Ping(context.TODO(), client.PingOptions{}) assert.Check(t, is.DeepEqual(expectedHeaders, apiclient.(*client.Client).CustomHTTPHeaders()))
assert.NilError(t, err) assert.Check(t, is.Equal(api.DefaultVersion, apiclient.ClientVersion()))
assert.DeepEqual(t, received, expectedHeaders)
} }
func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) { func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
var received http.Header host := ":2375"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { opts := &flags.CommonOptions{Hosts: []string{host}}
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}}
configFile := &configfile.ConfigFile{ configFile := &configfile.ConfigFile{
HTTPHeaders: map[string]string{ HTTPHeaders: map[string]string{
"My-Header": "Custom-Value from config-file", "My-Header": "Custom-Value",
}, },
} }
apiclient, err := NewAPIClientFromFlags(opts, configFile)
// 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)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host) assert.Check(t, is.Equal("tcp://localhost"+host, apiclient.DaemonHost()))
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
expectedHeaders := http.Header{ expectedHeaders := map[string]string{
"One": []string{"one-value"}, "My-Header": "Custom-Value",
"Two": []string{"two,value"}, "User-Agent": UserAgent(),
"Three": []string{""},
"Four": []string{"four-value-override"},
"User-Agent": []string{UserAgent()},
} }
_, err = apiClient.Ping(context.TODO(), client.PingOptions{}) assert.Check(t, is.DeepEqual(expectedHeaders, apiclient.(*client.Client).CustomHTTPHeaders()))
assert.NilError(t, err) assert.Check(t, is.Equal(api.DefaultVersion, apiclient.ClientVersion()))
assert.DeepEqual(t, received, expectedHeaders)
} }
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) { func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
const customVersion = "v3.3" customVersion := "v3.3.3"
const expectedVersion = "3.3" defer env.Patch(t, "DOCKER_API_VERSION", customVersion)()
t.Setenv("DOCKER_API_VERSION", customVersion) defer env.Patch(t, "DOCKER_HOST", ":2375")()
t.Setenv("DOCKER_HOST", ":2375")
opts := &flags.ClientOptions{} opts := &flags.CommonOptions{}
configFile := &configfile.ConfigFile{} configFile := &configfile.ConfigFile{}
apiclient, err := NewAPIClientFromFlags(opts, configFile) apiclient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, apiclient.ClientVersion(), expectedVersion) assert.Check(t, is.Equal(customVersion, apiclient.ClientVersion()))
} }
func TestNewAPIClientFromFlagsWithInvalidAPIVersionFromEnv(t *testing.T) { func TestNewAPIClientFromFlagsWithHttpProxyEnv(t *testing.T) {
t.Setenv("DOCKER_API_VERSION", "1") defer env.Patch(t, "HTTP_PROXY", "http://proxy.acme.com:1234")()
t.Setenv("DOCKER_HOST", ":2375") defer env.Patch(t, "DOCKER_HOST", "tcp://docker.acme.com:2376")()
apiClient, err := NewAPIClientFromFlags(&flags.ClientOptions{}, &configfile.ConfigFile{}) opts := &flags.CommonOptions{}
assert.ErrorContains(t, err, "invalid API version") configFile := &configfile.ConfigFile{}
assert.Check(t, apiClient == nil) 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 { type fakeClient struct {
client.Client client.Client
pingFunc func() (client.PingResult, error) pingFunc func() (types.Ping, error)
version string version string
negotiated bool negotiated bool
} }
func (c *fakeClient) Ping(_ context.Context, options client.PingOptions) (client.PingResult, error) { func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) {
res, err := c.pingFunc() return c.pingFunc()
if options.NegotiateAPIVersion {
if res.APIVersion != "" {
if c.negotiated || options.ForceNegotiate {
c.negotiated = true
}
}
}
return res, err
} }
func (c *fakeClient) ClientVersion() string { func (c *fakeClient) ClientVersion() string {
return c.version return c.version
} }
func TestInitializeFromClient(t *testing.T) { func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
const defaultVersion = "v1.55" c.negotiated = true
}
testcases := []struct { func TestInitializeFromClient(t *testing.T) {
defaultVersion := "v1.55"
var testcases = []struct {
doc string doc string
pingFunc func() (client.PingResult, error) pingFunc func() (types.Ping, error)
expectedServer ServerInfo expectedServer ServerInfo
negotiated bool negotiated bool
}{ }{
{ {
doc: "successful ping", doc: "successful ping",
pingFunc: func() (client.PingResult, error) { pingFunc: func() (types.Ping, error) {
return client.PingResult{Experimental: true, OSType: "linux", APIVersion: "v1.44"}, nil return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil
}, },
expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"}, expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
negotiated: true, negotiated: true,
}, },
{ {
doc: "failed ping, no API version", doc: "failed ping, no API version",
pingFunc: func() (client.PingResult, error) { pingFunc: func() (types.Ping, error) {
return client.PingResult{}, errors.New("failed") return types.Ping{}, errors.New("failed")
}, },
expectedServer: ServerInfo{HasExperimental: true}, expectedServer: ServerInfo{HasExperimental: true},
}, },
{ {
doc: "failed ping, with API version", doc: "failed ping, with API version",
pingFunc: func() (client.PingResult, error) { pingFunc: func() (types.Ping, error) {
return client.PingResult{APIVersion: "v1.44"}, errors.New("failed") return types.Ping{APIVersion: "v1.33"}, errors.New("failed")
}, },
expectedServer: ServerInfo{HasExperimental: true}, expectedServer: ServerInfo{HasExperimental: true},
negotiated: true, negotiated: true,
}, },
} }
for _, tc := range testcases { for _, testcase := range testcases {
t.Run(tc.doc, func(t *testing.T) { t.Run(testcase.doc, func(t *testing.T) {
apiClient := &fakeClient{ apiclient := &fakeClient{
pingFunc: tc.pingFunc, pingFunc: testcase.pingFunc,
version: defaultVersion, version: defaultVersion,
} }
cli := &DockerCli{client: apiClient} cli := &DockerCli{client: apiclient}
err := cli.Initialize(flags.NewClientOptions()) cli.initializeFromClient()
assert.NilError(t, err) assert.Check(t, is.DeepEqual(testcase.expectedServer, cli.serverInfo))
assert.DeepEqual(t, cli.ServerInfo(), tc.expectedServer) assert.Check(t, is.Equal(testcase.negotiated, apiclient.negotiated))
assert.Equal(t, apiClient.negotiated, tc.negotiated)
}) })
} }
} }
// Makes sure we don't hang forever on the initial connection. func TestExperimentalCLI(t *testing.T) {
// https://github.com/docker/cli/issues/3652 defaultVersion := "v1.55"
func TestInitializeFromClientHangs(t *testing.T) {
const (
// Sized against measured scheduler stalls:
// under CPU pressure this test sees 40-90ms stalls;
// this should give about 5x headroom.
// See https://github.com/docker/cli/issues/6003.
clientInitTimeout = 500 * time.Millisecond
// This is only a backstop against a genuine hang. var testcases = []struct {
// It should never be reached on a healthy run. doc string
// So, it should be fine to have a lenient timeout here. configfile string
waitTimeout = 10 * time.Second expectedExperimentalCLI bool
) }{
{
tmpDir := t.TempDir() doc: "default",
socket := filepath.Join(tmpDir, "my.sock") configfile: `{}`,
l, err := net.Listen("unix", socket) expectedExperimentalCLI: false,
assert.NilError(t, err) },
{
// Buffered, so the handler can record doc: "experimental",
// that it was reached without a reader configfile: `{
// having to be ready at that instant. "experimental": "enabled"
receivedReqCh := make(chan struct{}, 1) }`,
releaseHandlerCh := make(chan struct{}) expectedExperimentalCLI: true,
},
// Simulate a server that hangs on connections.
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case receivedReqCh <- struct{}{}:
default:
}
<-releaseHandlerCh
}))
ts.Listener = l
ts.Start()
t.Cleanup(func() {
close(releaseHandlerCh)
ts.Close()
})
opts := &flags.ClientOptions{Hosts: []string{"unix://" + socket}}
configFile := &configfile.ConfigFile{}
apiClient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err)
initErrCh := make(chan error, 1)
go func() {
cli := &DockerCli{client: apiClient, initTimeout: clientInitTimeout}
err := cli.Initialize(flags.NewClientOptions())
cli.CurrentVersion()
initErrCh <- err
}()
select {
case err := <-initErrCh:
assert.Check(t, err)
case <-time.After(waitTimeout):
t.Fatal("timeout waiting for initialization to complete")
} }
select { for _, testcase := range testcases {
case <-receivedReqCh: t.Run(testcase.doc, func(t *testing.T) {
case <-time.After(waitTimeout): dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile))
t.Fatal("server never received an init request") defer dir.Remove()
apiclient := &fakeClient{
version: defaultVersion,
pingFunc: func() (types.Ping, error) {
return types.Ping{Experimental: true, OSType: "linux", APIVersion: defaultVersion}, nil
},
}
cli := &DockerCli{client: apiclient, err: 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)
})
} }
} }
func TestNewDockerCliAndOperators(t *testing.T) { func TestNewDockerCliAndOperators(t *testing.T) {
outbuf := bytes.NewBuffer(nil) // Test default operations and also overriding default ones
errbuf := bytes.NewBuffer(nil)
cli, err := NewDockerCli( cli, err := NewDockerCli(
WithInputStream(io.NopCloser(strings.NewReader("some input"))), WithContentTrust(true),
WithOutputStream(outbuf), WithContainerizedClient(func(string) (clitypes.ContainerizedClient, error) { return nil, nil }),
WithErrorStream(errbuf),
) )
assert.NilError(t, err) assert.NilError(t, err)
// Check streams are initialized // Check streams are initialized
assert.Check(t, cli.In() != nil) assert.Check(t, cli.In() != nil)
assert.Check(t, cli.Out() != nil) assert.Check(t, cli.Out() != nil)
assert.Check(t, cli.Err() != nil) assert.Check(t, cli.Err() != nil)
inputStream, err := io.ReadAll(cli.In()) assert.Equal(t, cli.ContentTrustEnabled(), true)
client, err := cli.NewContainerizedEngineClient("")
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, string(inputStream), "some input") assert.Equal(t, client, nil)
// Check output stream // Apply can modify a dockerCli after construction
_, err = fmt.Fprint(cli.Out(), "output") inbuf := bytes.NewBuffer([]byte("input"))
outbuf := bytes.NewBuffer(nil)
errbuf := bytes.NewBuffer(nil)
cli.Apply(
WithInputStream(ioutil.NopCloser(inbuf)),
WithOutputStream(outbuf),
WithErrorStream(errbuf),
)
// Check input stream
inputStream, err := ioutil.ReadAll(cli.In())
assert.NilError(t, err) assert.NilError(t, err)
outputStream, err := io.ReadAll(outbuf) assert.Equal(t, string(inputStream), "input")
// Check output stream
fmt.Fprintf(cli.Out(), "output")
outputStream, err := ioutil.ReadAll(outbuf)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, string(outputStream), "output") assert.Equal(t, string(outputStream), "output")
// Check error stream // Check error stream
_, err = fmt.Fprint(cli.Err(), "error") fmt.Fprintf(cli.Err(), "error")
assert.NilError(t, err) errStream, err := ioutil.ReadAll(errbuf)
errStream, err := io.ReadAll(errbuf)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, string(errStream), "error") assert.Equal(t, string(errStream), "error")
} }
@@ -319,127 +318,8 @@ func TestNewDockerCliAndOperators(t *testing.T) {
func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) { func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
cli, err := NewDockerCli() cli, err := NewDockerCli()
assert.NilError(t, err) assert.NilError(t, err)
apiClient, err := client.New() assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithInitializeClient(func(cli *DockerCli) (client.APIClient, error) {
assert.NilError(t, err) return client.NewClientWithOpts()
assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithAPIClient(apiClient))) })))
assert.Check(t, cli.ContextStore() != nil) assert.Check(t, cli.ContextStore() != nil)
} }
func TestHooksEnabled(t *testing.T) {
t.Run("disabled by default", func(t *testing.T) {
// Make sure we don't depend on any existing ~/.docker/config.json
config.SetDir(t.TempDir())
cli, err := NewDockerCli()
assert.NilError(t, err)
assert.Check(t, !cli.HooksEnabled())
})
t.Run("enabled in configFile", func(t *testing.T) {
configFile := `{
"features": {
"hooks": "true"
}}`
config.SetDir(t.TempDir())
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
assert.NilError(t, err)
cli, err := NewDockerCli()
assert.NilError(t, err)
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")
config.SetDir(t.TempDir())
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
assert.NilError(t, err)
cli, err := NewDockerCli()
assert.NilError(t, err)
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")
config.SetDir(t.TempDir())
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
assert.NilError(t, err)
cli, err := NewDockerCli()
assert.NilError(t, err)
assert.Check(t, !cli.HooksEnabled())
})
}
func TestSetGoDebug(t *testing.T) {
t.Run("GODEBUG already set", func(t *testing.T) {
t.Setenv("GODEBUG", "val1,val2")
meta := store.Metadata{}
setGoDebug(meta)
assert.Equal(t, "val1,val2", os.Getenv("GODEBUG"))
})
t.Run("GODEBUG in context metadata can set env", func(t *testing.T) {
t.Setenv("GODEBUG", "")
meta := store.Metadata{
Metadata: DockerContext{
AdditionalFields: map[string]any{
"GODEBUG": "val1,val2=1",
},
},
}
setGoDebug(meta)
assert.Equal(t, "val1,val2=1", os.Getenv("GODEBUG"))
})
}
func TestNewDockerCliWithCustomUserAgent(t *testing.T) {
var received string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
received = r.UserAgent()
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
opts := &flags.ClientOptions{Hosts: []string{host}}
cli, err := NewDockerCli(
WithUserAgent("fake-agent/0.0.1"),
)
assert.NilError(t, err)
cli.currentContext = DefaultContextName
cli.options = opts
cli.configFile = &configfile.ConfigFile{}
_, err = cli.Client().Ping(context.TODO(), client.PingOptions{})
assert.NilError(t, err)
assert.DeepEqual(t, received, "fake-agent/0.0.1")
}
func TestNewDockerCliWithAPIClientOptions(t *testing.T) {
var received string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
received = r.UserAgent()
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
opts := &flags.ClientOptions{Hosts: []string{host}}
cli, err := NewDockerCli(
WithAPIClientOptions(client.WithUserAgent("fake-agent/0.0.1")),
)
assert.NilError(t, err)
cli.currentContext = DefaultContextName
cli.options = opts
cli.configFile = &configfile.ConfigFile{}
_, err = cli.Client().Ping(t.Context(), client.PingOptions{})
assert.NilError(t, err)
assert.DeepEqual(t, received, "fake-agent/0.0.1")
}
+137 -21
View File
@@ -1,30 +1,146 @@
package commands package commands
import ( import (
"os"
"runtime"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
_ "github.com/docker/cli/cli/command/builder" "github.com/docker/cli/cli/command/builder"
_ "github.com/docker/cli/cli/command/checkpoint" "github.com/docker/cli/cli/command/checkpoint"
_ "github.com/docker/cli/cli/command/config" "github.com/docker/cli/cli/command/config"
_ "github.com/docker/cli/cli/command/container" "github.com/docker/cli/cli/command/container"
_ "github.com/docker/cli/cli/command/context" "github.com/docker/cli/cli/command/context"
_ "github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/command/engine"
_ "github.com/docker/cli/cli/command/manifest" "github.com/docker/cli/cli/command/image"
_ "github.com/docker/cli/cli/command/network" "github.com/docker/cli/cli/command/manifest"
_ "github.com/docker/cli/cli/command/node" "github.com/docker/cli/cli/command/network"
_ "github.com/docker/cli/cli/command/plugin" "github.com/docker/cli/cli/command/node"
_ "github.com/docker/cli/cli/command/registry" "github.com/docker/cli/cli/command/plugin"
_ "github.com/docker/cli/cli/command/secret" "github.com/docker/cli/cli/command/registry"
_ "github.com/docker/cli/cli/command/service" "github.com/docker/cli/cli/command/secret"
_ "github.com/docker/cli/cli/command/stack" "github.com/docker/cli/cli/command/service"
_ "github.com/docker/cli/cli/command/swarm" "github.com/docker/cli/cli/command/stack"
_ "github.com/docker/cli/cli/command/system" "github.com/docker/cli/cli/command/swarm"
_ "github.com/docker/cli/cli/command/volume" "github.com/docker/cli/cli/command/system"
"github.com/docker/cli/internal/commands" "github.com/docker/cli/cli/command/trust"
"github.com/docker/cli/cli/command/volume"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func AddCommands(cmd *cobra.Command, dockerCLI command.Cli) { // AddCommands adds all the commands from cli/command to the root command
for _, c := range commands.Commands() { func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
cmd.AddCommand(c(dockerCLI)) cmd.AddCommand(
// checkpoint
checkpoint.NewCheckpointCommand(dockerCli),
// config
config.NewConfigCommand(dockerCli),
// container
container.NewContainerCommand(dockerCli),
container.NewRunCommand(dockerCli),
// image
image.NewImageCommand(dockerCli),
image.NewBuildCommand(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),
// 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)),
hide(container.NewStartCommand(dockerCli)),
hide(container.NewStatsCommand(dockerCli)),
hide(container.NewStopCommand(dockerCli)),
hide(container.NewTopCommand(dockerCli)),
hide(container.NewUnpauseCommand(dockerCli)),
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)),
)
if runtime.GOOS == "linux" {
// engine
cmd.AddCommand(engine.NewEngineCommand(dockerCli))
} }
} }
func hide(cmd *cobra.Command) *cobra.Command {
// If the environment variable with name "DOCKER_HIDE_LEGACY_COMMANDS" is not empty,
// these legacy commands (such as `docker ps`, `docker exec`, etc)
// will not be shown in output console.
if os.Getenv("DOCKER_HIDE_LEGACY_COMMANDS") == "" {
return cmd
}
cmdCopy := *cmd
cmdCopy.Hidden = true
cmdCopy.Aliases = []string{}
return &cmdCopy
}
-282
View File
@@ -1,282 +0,0 @@
package completion
import (
"os"
"strings"
"github.com/distribution/reference"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
// APIClientProvider provides a method to get a [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, limit int) cobra.CompletionFunc {
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if limit > 0 && len(args) >= limit {
return nil, cobra.ShellCompDirectiveNoFileComp
}
res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
for _, img := range res.Items {
names = append(names, img.RepoTags...)
}
return names, cobra.ShellCompDirectiveNoFileComp
})
}
// ImageNamesWithBase offers completion for images present within the local store,
// including both full image names with tags and base image names (repository names only)
// when multiple tags exist for the same base name
func ImageNamesWithBase(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if limit > 0 && len(args) >= limit {
return nil, cobra.ShellCompDirectiveNoFileComp
}
res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
baseNameCounts := make(map[string]int)
for _, img := range res.Items {
names = append(names, img.RepoTags...)
for _, tag := range img.RepoTags {
ref, err := reference.ParseNormalizedNamed(tag)
if err != nil {
continue
}
baseNameCounts[reference.FamiliarName(ref)]++
}
}
for baseName, count := range baseNameCounts {
if count > 1 {
names = append(names, baseName)
}
}
return names, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
})
}
// ContainerNames offers completion for container names and IDs
// By default, only names are returned.
// Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs.
func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(container.Summary) bool) cobra.CompletionFunc {
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().ContainerList(cmd.Context(), client.ContainerListOptions{
All: all,
})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
showContainerIDs := os.Getenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS") == "yes"
var names []string
for _, ctr := range res.Items {
skip := false
for _, fn := range filters {
if fn != nil && !fn(ctr) {
skip = true
break
}
}
if skip {
continue
}
if showContainerIDs {
names = append(names, ctr.ID)
}
for _, n := range ctr.Names {
// Skip legacy link names: "/linked-container/link-name"
if len(n) <= 1 || strings.IndexByte(n[1:], '/') != -1 {
continue
}
names = append(names, strings.TrimPrefix(n, "/"))
}
}
return names, cobra.ShellCompDirectiveNoFileComp
})
}
// VolumeNames offers completion for volumes
func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().VolumeList(cmd.Context(), client.VolumeListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
for _, vol := range res.Items {
names = append(names, vol.Name)
}
return names, cobra.ShellCompDirectiveNoFileComp
})
}
// NetworkNames offers completion for networks
func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().NetworkList(cmd.Context(), client.NetworkListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
for _, nw := range res.Items {
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.CompletionFunc {
return Unique(func(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
envs := os.Environ()
names = make([]string, 0, len(envs))
for _, env := range envs {
name, _, _ := strings.Cut(env, "=")
names = append(names, name)
}
return names, cobra.ShellCompDirectiveNoFileComp
})
}
// FromList offers completion for the given list of options.
func FromList(options ...string) cobra.CompletionFunc {
return Unique(cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp))
}
// WithPrefix prefixes every element in the slice with the given prefix.
// It is a helper for building "--filter" completions, where each candidate
// value is offered as "key=value".
func WithPrefix(prefix string, values []string) []string {
result := make([]string, len(values))
for i, v := range values {
result[i] = prefix + v
}
return result
}
// WithSuffix appends the given suffix to every element in the slice. It is a
// helper for building "--filter" completions, where filter keys are offered
// with a trailing "=" (combined with [cobra.ShellCompDirectiveNoSpace]).
func WithSuffix(suffix string, values []string) []string {
result := make([]string, len(values))
for i, v := range values {
result[i] = v + suffix
}
return result
}
// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault],
// which indicates to let the shell perform its default behavior after
// completions have been provided.
func FileNames() cobra.CompletionFunc {
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveDefault
}
}
var commonPlatforms = []string{
"linux/386",
"linux/amd64",
"linux/arm",
"linux/arm/v5",
"linux/arm/v6",
"linux/arm/v7",
"linux/arm64",
"linux/arm64/v8",
// IBM power and z platforms
"linux/ppc64le",
"linux/s390x",
// Not yet supported
"linux/riscv64",
"windows/amd64",
"wasip1/wasm",
}
// Platforms offers completion for platform-strings. It provides a non-exhaustive
// list of platforms to be used for completion. Platform-strings are based on
// [runtime.GOOS] and [runtime.GOARCH], but with (optional) variants added. A
// list of recognised os/arch combinations from the Go runtime can be obtained
// through "go tool dist list".
//
// Some noteworthy exclusions from this list:
//
// - arm64 images ("windows/arm64", "windows/arm64/v8") do not yet exist for windows.
// - we don't (yet) include `os-variant` for completion (as can be used for Windows images)
// - we don't (yet) include platforms for which we don't build binaries, such as
// BSD platforms (freebsd, netbsd, openbsd), android, macOS (darwin).
// - we currently exclude architectures that may have unofficial builds,
// but don't have wide adoption (and no support), such as loong64, mipsXXX,
// ppc64 (non-le) to prevent confusion.
func Platforms() cobra.CompletionFunc {
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
}
}
// Unique wraps a completion func and removes completion results that are
// already consumed (i.e., appear in "args").
//
// For example:
//
// # initial completion: args is empty, so all results are shown
// command <tab>
// one two three
//
// # "one" is already used so omitted
// command one <tab>
// two three
func Unique(fn cobra.CompletionFunc) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
all, dir := fn(cmd, args, toComplete)
if len(all) == 0 || len(args) == 0 {
return all, dir
}
alreadyCompleted := make(map[string]struct{}, len(args))
for _, a := range args {
alreadyCompleted[a] = struct{}{}
}
out := make([]string, 0, len(all))
for _, c := range all {
if _, ok := alreadyCompleted[c]; !ok {
out = append(out, c)
}
}
return out, dir
}
}
-386
View File
@@ -1,386 +0,0 @@
package completion
import (
"context"
"errors"
"testing"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/volume"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/env"
)
type fakeCLI struct {
*fakeClient
}
// Client implements [APIClientProvider].
func (c fakeCLI) Client() client.APIClient {
return c.fakeClient
}
type fakeClient struct {
client.Client
containerListFunc func(context.Context, client.ContainerListOptions) (client.ContainerListResult, error)
imageListFunc func(context.Context, client.ImageListOptions) (client.ImageListResult, error)
networkListFunc func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error)
volumeListFunc func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error)
}
func (c *fakeClient) ContainerList(ctx context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) {
if c.containerListFunc != nil {
return c.containerListFunc(ctx, options)
}
return client.ContainerListResult{}, nil
}
func (c *fakeClient) ImageList(ctx context.Context, options client.ImageListOptions) (client.ImageListResult, error) {
if c.imageListFunc != nil {
return c.imageListFunc(ctx, options)
}
return client.ImageListResult{}, nil
}
func (c *fakeClient) NetworkList(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) {
if c.networkListFunc != nil {
return c.networkListFunc(ctx, options)
}
return client.NetworkListResult{}, nil
}
func (c *fakeClient) VolumeList(ctx context.Context, options client.VolumeListOptions) (client.VolumeListResult, error) {
if c.volumeListFunc != nil {
return c.volumeListFunc(ctx, options)
}
return client.VolumeListResult{}, nil
}
func TestCompleteContainerNames(t *testing.T) {
tests := []struct {
doc string
showAll, showIDs bool
filters []func(container.Summary) bool
containers []container.Summary
expOut []string
expOpts client.ContainerListOptions
expDirective cobra.ShellCompDirective
}{
{
doc: "no results",
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "all containers",
showAll: true,
containers: []container.Summary{
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
},
expOut: []string{"container-c", "container-b", "container-a"},
expOpts: client.ContainerListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "all containers with ids",
showAll: true,
showIDs: true,
containers: []container.Summary{
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
},
expOut: []string{"id-c", "container-c", "id-b", "container-b", "id-a", "container-a"},
expOpts: client.ContainerListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "only running containers",
showAll: false,
containers: []container.Summary{
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
},
expOut: []string{"container-c"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with filter",
showAll: true,
filters: []func(container.Summary) bool{
func(ctr container.Summary) bool { return ctr.State == container.StateCreated },
},
containers: []container.Summary{
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c"}},
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
},
expOut: []string{"container-b"},
expOpts: client.ContainerListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "multiple filters",
showAll: true,
filters: []func(container.Summary) bool{
func(ctr container.Summary) bool { return ctr.ID == "id-a" },
func(ctr container.Summary) bool { return ctr.State == container.StateCreated },
},
containers: []container.Summary{
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c"}},
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
{ID: "id-a", State: container.StateCreated, Names: []string{"/container-a"}},
},
expOut: []string{"container-a"},
expOpts: client.ContainerListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with error",
expDirective: cobra.ShellCompDirectiveError,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
if tc.showIDs {
t.Setenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS", "yes")
}
comp := ContainerNames(fakeCLI{&fakeClient{
containerListFunc: func(_ context.Context, opts client.ContainerListOptions) (client.ContainerListResult, error) {
assert.Check(t, is.DeepEqual(opts, tc.expOpts))
if tc.expDirective == cobra.ShellCompDirectiveError {
return client.ContainerListResult{}, errors.New("some error occurred")
}
return client.ContainerListResult{Items: tc.containers}, nil
},
}}, tc.showAll, tc.filters...)
containers, directives := comp(&cobra.Command{}, nil, "")
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
assert.Check(t, is.DeepEqual(containers, tc.expOut))
})
}
}
func TestCompleteEnvVarNames(t *testing.T) {
env.PatchAll(t, map[string]string{
"ENV_A": "hello-a",
"ENV_B": "hello-b",
})
values, directives := EnvVarNames()(nil, nil, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
expected := []string{"ENV_A", "ENV_B"}
assert.Check(t, is.DeepEqual(values, expected, cmpopts.SortSlices(func(a, b string) bool {
return a < b
})))
}
func TestCompleteFileNames(t *testing.T) {
values, directives := FileNames()(nil, nil, "")
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveDefault))
assert.Check(t, is.Len(values, 0))
}
func TestCompleteFromList(t *testing.T) {
expected := []string{"one", "two", "three"}
values, directives := FromList(expected...)(nil, nil, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
assert.Check(t, is.DeepEqual(values, expected))
}
func TestWithPrefix(t *testing.T) {
assert.Check(t, is.DeepEqual(WithPrefix("node=", []string{"n1", "n2"}), []string{"node=n1", "node=n2"}))
assert.Check(t, is.DeepEqual(WithPrefix("node=", []string{}), []string{}))
assert.Check(t, is.DeepEqual(WithPrefix("", []string{"n1"}), []string{"n1"}))
}
func TestWithSuffix(t *testing.T) {
assert.Check(t, is.DeepEqual(WithSuffix("=", []string{"id", "name"}), []string{"id=", "name="}))
assert.Check(t, is.DeepEqual(WithSuffix("=", []string{}), []string{}))
assert.Check(t, is.DeepEqual(WithSuffix("", []string{"id"}), []string{"id"}))
}
func TestCompleteImageNames(t *testing.T) {
tests := []struct {
doc string
images []image.Summary
expOut []string
expDirective cobra.ShellCompDirective
}{
{
doc: "no results",
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with results",
images: []image.Summary{
{RepoTags: []string{"image-c:latest", "image-c:other"}},
{RepoTags: []string{"image-b:latest", "image-b:other"}},
{RepoTags: []string{"image-a:latest", "image-a:other"}},
},
expOut: []string{"image-c:latest", "image-c:other", "image-b:latest", "image-b:other", "image-a:latest", "image-a:other"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with error",
expDirective: cobra.ShellCompDirectiveError,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
comp := ImageNames(fakeCLI{&fakeClient{
imageListFunc: func(context.Context, client.ImageListOptions) (client.ImageListResult, error) {
if tc.expDirective == cobra.ShellCompDirectiveError {
return client.ImageListResult{}, errors.New("some error occurred")
}
return client.ImageListResult{Items: tc.images}, nil
},
}}, -1)
volumes, directives := comp(&cobra.Command{}, nil, "")
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
})
}
}
func TestCompleteNetworkNames(t *testing.T) {
tests := []struct {
doc string
networks []network.Summary
expOut []string
expDirective cobra.ShellCompDirective
}{
{
doc: "no results",
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with results",
networks: []network.Summary{
{
Network: network.Network{
ID: "nw-c",
Name: "network-c",
},
},
{
Network: network.Network{
ID: "nw-b",
Name: "network-b",
},
},
{
Network: network.Network{
ID: "nw-a",
Name: "network-a",
},
},
},
expOut: []string{"network-c", "network-b", "network-a"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with error",
expDirective: cobra.ShellCompDirectiveError,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
comp := NetworkNames(fakeCLI{&fakeClient{
networkListFunc: func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error) {
if tc.expDirective == cobra.ShellCompDirectiveError {
return client.NetworkListResult{}, errors.New("some error occurred")
}
return client.NetworkListResult{Items: tc.networks}, nil
},
}})
volumes, directives := comp(&cobra.Command{}, nil, "")
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
})
}
}
func TestCompletePlatforms(t *testing.T) {
values, directives := Platforms()(nil, nil, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
assert.Check(t, is.DeepEqual(values, commonPlatforms))
}
func TestCompleteVolumeNames(t *testing.T) {
tests := []struct {
doc string
volumes []volume.Volume
expOut []string
expDirective cobra.ShellCompDirective
}{
{
doc: "no results",
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with results",
volumes: []volume.Volume{
{Name: "volume-c"},
{Name: "volume-b"},
{Name: "volume-a"},
},
expOut: []string{"volume-c", "volume-b", "volume-a"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with error",
expDirective: cobra.ShellCompDirectiveError,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
comp := VolumeNames(fakeCLI{&fakeClient{
volumeListFunc: func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error) {
if tc.expDirective == cobra.ShellCompDirectiveError {
return client.VolumeListResult{}, errors.New("some error occurred")
}
return client.VolumeListResult{Items: tc.volumes}, nil
},
}})
volumes, directives := comp(&cobra.Command{}, nil, "")
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
})
}
}
func TestUnique(t *testing.T) {
base := []string{"alpha", "beta", "gamma"}
comp := Unique(func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
return base, cobra.ShellCompDirectiveNoFileComp
})
values, directives := comp(&cobra.Command{}, []string{"beta"}, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp))
assert.Check(t, is.DeepEqual(values, []string{"alpha", "gamma"}))
assert.Check(t, is.DeepEqual(base, []string{"alpha", "beta", "gamma"}))
values, directives = comp(&cobra.Command{}, []string{"gamma"}, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp))
assert.Check(t, is.DeepEqual(values, []string{"alpha", "beta"}))
assert.Check(t, is.DeepEqual(base, []string{"alpha", "beta", "gamma"}))
}
+19 -17
View File
@@ -3,41 +3,43 @@ package config
import ( import (
"context" "context"
"github.com/moby/moby/client" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
) )
type fakeClient struct { type fakeClient struct {
client.Client client.Client
configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error) configCreateFunc func(swarm.ConfigSpec) (types.ConfigCreateResponse, error)
configInspectFunc func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error) configInspectFunc func(string) (swarm.Config, []byte, error)
configListFunc func(context.Context, client.ConfigListOptions) (client.ConfigListResult, error) configListFunc func(types.ConfigListOptions) ([]swarm.Config, error)
configRemoveFunc func(context.Context, string, client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) configRemoveFunc func(string) error
} }
func (c *fakeClient) ConfigCreate(ctx context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) { func (c *fakeClient) ConfigCreate(ctx context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if c.configCreateFunc != nil { if c.configCreateFunc != nil {
return c.configCreateFunc(ctx, options) return c.configCreateFunc(spec)
} }
return client.ConfigCreateResult{}, nil return types.ConfigCreateResponse{}, nil
} }
func (c *fakeClient) ConfigInspect(ctx context.Context, id string, options client.ConfigInspectOptions) (client.ConfigInspectResult, error) { func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
if c.configInspectFunc != nil { if c.configInspectFunc != nil {
return c.configInspectFunc(ctx, id, options) return c.configInspectFunc(id)
} }
return client.ConfigInspectResult{}, nil return swarm.Config{}, nil, nil
} }
func (c *fakeClient) ConfigList(ctx context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { func (c *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
if c.configListFunc != nil { if c.configListFunc != nil {
return c.configListFunc(ctx, options) return c.configListFunc(options)
} }
return client.ConfigListResult{}, nil return []swarm.Config{}, nil
} }
func (c *fakeClient) ConfigRemove(ctx context.Context, name string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) { func (c *fakeClient) ConfigRemove(ctx context.Context, name string) error {
if c.configRemoveFunc != nil { if c.configRemoveFunc != nil {
return c.configRemoveFunc(ctx, name, options) return c.configRemoveFunc(name)
} }
return client.ConfigRemoveResult{}, nil return nil
} }
+11 -33
View File
@@ -1,51 +1,29 @@
package config package config
import ( import (
"github.com/spf13/cobra"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/internal/commands"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
) )
func init() { // NewConfigCommand returns a cobra command for `config` subcommands
commands.Register(newConfigCommand) func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
}
// newConfigCommand returns a cobra command for `config` subcommands
func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "config", Use: "config",
Short: "Manage Swarm configs", Short: "Manage Docker configs",
Args: cli.NoArgs, Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCLI.Err()), RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{ Annotations: map[string]string{
"version": "1.30", "version": "1.30",
"swarm": "manager", "swarm": "",
}, },
DisableFlagsInUseLine: true,
} }
cmd.AddCommand( cmd.AddCommand(
newConfigListCommand(dockerCLI), newConfigListCommand(dockerCli),
newConfigCreateCommand(dockerCLI), newConfigCreateCommand(dockerCli),
newConfigInspectCommand(dockerCLI), newConfigInspectCommand(dockerCli),
newConfigRemoveCommand(dockerCLI), newConfigRemoveCommand(dockerCli),
) )
return cmd return cmd
} }
// completeNames offers completion for swarm configs
func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().ConfigList(cmd.Context(), client.ConfigListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
for _, config := range res.Items {
names = append(names, config.ID)
}
return names, cobra.ShellCompDirectiveNoFileComp
}
}
+41 -98
View File
@@ -2,30 +2,30 @@ package config
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/moby/moby/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/moby/moby/client" "github.com/docker/docker/pkg/system"
"github.com/moby/sys/sequential" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// createOptions specifies some options that are used when creating a config. // CreateOptions specifies some options that are used when creating a config.
type createOptions struct { type CreateOptions struct {
name string Name string
templateDriver string TemplateDriver string
file string File string
labels opts.ListOpts Labels opts.ListOpts
} }
func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command { func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
createOpts := createOptions{ createOpts := CreateOptions{
labels: opts.NewListOpts(opts.ValidateLabel), Labels: opts.NewListOpts(opts.ValidateLabel),
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -33,113 +33,56 @@ func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command {
Short: "Create a config from a file or STDIN", Short: "Create a config from a file or STDIN",
Args: cli.ExactArgs(2), Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
createOpts.name = args[0] createOpts.Name = args[0]
createOpts.file = args[1] createOpts.File = args[1]
return runCreate(cmd.Context(), dockerCLI, createOpts) return RunConfigCreate(dockerCli, createOpts)
}, },
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
switch len(args) {
case 0:
// No completion for the first argument, which is the name for
// the new config, but if a non-empty name is given, we return
// it as completion to allow "tab"-ing to the next completion.
return []string{toComplete}, cobra.ShellCompDirectiveNoFileComp
case 1:
// Second argument is either "-" or a file to load.
//
// TODO(thaJeztah): provide completion for "-".
return nil, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveDefault
default:
// Command only accepts two arguments.
return nil, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
}
},
DisableFlagsInUseLine: true,
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.VarP(&createOpts.labels, "label", "l", "Config labels") flags.VarP(&createOpts.Labels, "label", "l", "Config labels")
flags.StringVar(&createOpts.templateDriver, "template-driver", "", "Template driver") flags.StringVar(&createOpts.TemplateDriver, "template-driver", "", "Template driver")
_ = flags.SetAnnotation("template-driver", "version", []string{"1.37"}) flags.SetAnnotation("template-driver", "version", []string{"1.37"})
return cmd return cmd
} }
// runCreate creates a config with the given options. // RunConfigCreate creates a config with the given options.
func runCreate(ctx context.Context, dockerCLI command.Cli, options createOptions) error { func RunConfigCreate(dockerCli command.Cli, options CreateOptions) error {
apiClient := dockerCLI.Client() client := dockerCli.Client()
ctx := context.Background()
configData, err := readConfigData(dockerCLI.In(), options.file) var in io.Reader = dockerCli.In()
if options.File != "-" {
file, err := system.OpenSequential(options.File)
if err != nil {
return err
}
in = file
defer file.Close()
}
configData, err := ioutil.ReadAll(in)
if err != nil { if err != nil {
return fmt.Errorf("error reading content from %q: %v", options.file, err) return errors.Errorf("Error reading content from %q: %v", options.File, err)
} }
spec := swarm.ConfigSpec{ spec := swarm.ConfigSpec{
Annotations: swarm.Annotations{ Annotations: swarm.Annotations{
Name: options.name, Name: options.Name,
Labels: opts.ConvertKVStringsToMap(options.labels.GetSlice()), Labels: opts.ConvertKVStringsToMap(options.Labels.GetAll()),
}, },
Data: configData, Data: configData,
} }
if options.templateDriver != "" { if options.TemplateDriver != "" {
spec.Templating = &swarm.Driver{ spec.Templating = &swarm.Driver{
Name: options.templateDriver, Name: options.TemplateDriver,
} }
} }
r, err := apiClient.ConfigCreate(ctx, client.ConfigCreateOptions{ r, err := client.ConfigCreate(ctx, spec)
Spec: spec,
})
if err != nil { if err != nil {
return err return err
} }
_, _ = fmt.Fprintln(dockerCLI.Out(), r.ID) fmt.Fprintln(dockerCli.Out(), r.ID)
return nil return nil
} }
// maxConfigSize is the maximum byte length of the [swarm.ConfigSpec.Data] field,
// as defined by [MaxConfigSize] in SwarmKit.
//
// [MaxConfigSize]: https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/manager/controlapi#MaxConfigSize
const maxConfigSize = 1000 * 1024 // 1000KB
// readConfigData reads the config from either stdin or the given fileName.
//
// It reads up to twice the maximum size of the config ([maxConfigSize]),
// just in case swarm's limit changes; this is only a safeguard to prevent
// reading arbitrary files into memory.
func readConfigData(in io.Reader, fileName string) ([]byte, error) {
switch fileName {
case "-":
data, err := io.ReadAll(io.LimitReader(in, 2*maxConfigSize))
if err != nil {
return nil, fmt.Errorf("error reading from STDIN: %w", err)
}
if len(data) == 0 {
return nil, errors.New("error reading from STDIN: data is empty")
}
return data, nil
case "":
return nil, errors.New("config file is required")
default:
// Open file with [FILE_FLAG_SEQUENTIAL_SCAN] on Windows, which
// prevents Windows from aggressively caching it. We expect this
// file to be only read once. Given that this is expected to be
// a small file, this may not be a significant optimization, so
// we could choose to omit this, and use a regular [os.Open].
//
// [FILE_FLAG_SEQUENTIAL_SCAN]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
f, err := sequential.Open(fileName)
if err != nil {
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
}
defer f.Close()
data, err := io.ReadAll(io.LimitReader(f, 2*maxConfigSize))
if err != nil {
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
}
if len(data) == 0 {
return nil, fmt.Errorf("error reading from %s: data is empty", fileName)
}
return data, nil
}
}
+43 -50
View File
@@ -1,22 +1,19 @@
package config package config
import ( import (
"context" "io/ioutil"
"errors"
"fmt"
"io"
"os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/swarm" "github.com/docker/docker/api/types"
"github.com/moby/moby/client" "github.com/docker/docker/api/types/swarm"
"gotest.tools/v3/assert" "github.com/pkg/errors"
is "gotest.tools/v3/assert/cmp" "gotest.tools/assert"
"gotest.tools/v3/golden" is "gotest.tools/assert/cmp"
"gotest.tools/golden"
) )
const configDataFile = "config-create-with-name.golden" const configDataFile = "config-create-with-name.golden"
@@ -24,53 +21,49 @@ const configDataFile = "config-create-with-name.golden"
func TestConfigCreateErrors(t *testing.T) { func TestConfigCreateErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error) configCreateFunc func(swarm.ConfigSpec) (types.ConfigCreateResponse, error)
expectedError string expectedError string
}{ }{
{ {
args: []string{"too_few"}, args: []string{"too_few"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {args: []string{"too", "many", "arguments"},
args: []string{"too", "many", "arguments"}, expectedError: "requires exactly 2 arguments",
expectedError: "requires 2 arguments",
}, },
{ {
args: []string{"name", filepath.Join("testdata", configDataFile)}, args: []string{"name", filepath.Join("testdata", configDataFile)},
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) { configCreateFunc: func(configSpec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
return client.ConfigCreateResult{}, errors.New("error creating config") return types.ConfigCreateResponse{}, errors.Errorf("error creating config")
}, },
expectedError: "error creating config", expectedError: "error creating config",
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.expectedError, func(t *testing.T) { cmd := newConfigCreateCommand(
cmd := newConfigCreateCommand( test.NewFakeCli(&fakeClient{
test.NewFakeCli(&fakeClient{ configCreateFunc: tc.configCreateFunc,
configCreateFunc: tc.configCreateFunc, }),
}), )
) cmd.SetArgs(tc.args)
cmd.SetArgs(tc.args) cmd.SetOutput(ioutil.Discard)
cmd.SetOut(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
cmd.SetErr(io.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
})
} }
} }
func TestConfigCreateWithName(t *testing.T) { func TestConfigCreateWithName(t *testing.T) {
const name = "config-with-name" name := "foo"
var actual []byte var actual []byte
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) { configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if options.Spec.Name != name { if spec.Name != name {
return client.ConfigCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name) return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
} }
actual = options.Spec.Data actual = spec.Data
return client.ConfigCreateResult{ return types.ConfigCreateResponse{
ID: "ID-" + options.Spec.Name, ID: "ID-" + spec.Name,
}, nil }, nil
}, },
}) })
@@ -87,9 +80,9 @@ func TestConfigCreateWithLabels(t *testing.T) {
"lbl1": "Label-foo", "lbl1": "Label-foo",
"lbl2": "Label-bar", "lbl2": "Label-bar",
} }
const name = "config-with-labels" name := "foo"
data, err := os.ReadFile(filepath.Join("testdata", configDataFile)) data, err := ioutil.ReadFile(filepath.Join("testdata", configDataFile))
assert.NilError(t, err) assert.NilError(t, err)
expected := swarm.ConfigSpec{ expected := swarm.ConfigSpec{
@@ -101,13 +94,13 @@ func TestConfigCreateWithLabels(t *testing.T) {
} }
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) { configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if !reflect.DeepEqual(options.Spec, expected) { if !reflect.DeepEqual(spec, expected) {
return client.ConfigCreateResult{}, fmt.Errorf("expected %+v, got %+v", expected, options.Spec) return types.ConfigCreateResponse{}, errors.Errorf("expected %+v, got %+v", expected, spec)
} }
return client.ConfigCreateResult{ return types.ConfigCreateResponse{
ID: "ID-" + options.Spec.Name, ID: "ID-" + spec.Name,
}, nil }, nil
}, },
}) })
@@ -124,20 +117,20 @@ func TestConfigCreateWithTemplatingDriver(t *testing.T) {
expectedDriver := &swarm.Driver{ expectedDriver := &swarm.Driver{
Name: "template-driver", Name: "template-driver",
} }
const name = "config-with-template-driver" name := "foo"
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) { configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if options.Spec.Name != name { if spec.Name != name {
return client.ConfigCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name) return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
} }
if options.Spec.Templating.Name != expectedDriver.Name { if spec.Templating.Name != expectedDriver.Name {
return client.ConfigCreateResult{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, options.Spec.Labels) return types.ConfigCreateResponse{}, errors.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
} }
return client.ConfigCreateResult{ return types.ConfigCreateResponse{
ID: "ID-" + options.Spec.Name, ID: "ID-" + spec.Name,
}, nil }, nil
}, },
}) })

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