Compare commits

..

36 Commits

Author SHA1 Message Date
55c4c88966 Merge pull request #2987 from thaJeztah/20.10_revert_backport_ignore_sigurg
[20.10] Revert "Ignore SIGURG on Linux."
2021-03-02 09:58:58 -08:00
f33a69f6ee [20.10] Revert "Ignore SIGURG on Linux."
This reverts commit 3c87f01b18.

This commit introduced two regressions;

- spurious "Unsupported signal: <nil>. Discarding."
- docker start --attach hanging if the container does not
  have a TTY attached

Reverting for now, while we dug deeper into what's causing
the regression.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-03-01 20:56:18 +01:00
d3cb89ee53 Merge pull request #2957 from thaJeztah/20.10_backport_docs_fixes
[20.10 backport] documentation and completion-script fixes
2021-02-18 12:03:39 +01:00
433014bcfb Merge pull request #2959 from thaJeztah/20.10_backport_fix_login_panic
[20.10 backport] Fix panic when failing to get DefaultAuthConfig
2021-02-18 12:02:41 +01:00
61cb016851 Merge pull request #2961 from thaJeztah/20.10_backport_fix_context_dockerfile_from_stdin_with_buildkit
[20.10 backport] Fix reading context and dockerfile from stdin with BuildKit
2021-02-18 12:02:09 +01:00
53c4602909 Merge pull request #2962 from thaJeztah/20.10_backport_go_md2man_binary_name
[20.10 backport] Rename bin/md2man to bin/go-md2man
2021-02-18 11:58:57 +01:00
830d6595e7 Merge pull request #2963 from thaJeztah/20.10_backport_fix_update_rollback_order
[20.10 backport] fix --update-order and --rollback-order flags
2021-02-18 11:58:23 +01:00
13048444cc Merge pull request #2964 from thaJeztah/20.10_backport_fix_swarm_rollback_exitcode
[20.10 backport] Fix swarm rollback exitcode, and fix skipping verify step
2021-02-18 11:55:40 +01:00
3e293e6bd1 Merge pull request #2958 from thaJeztah/20.10_backport_fix_homedir_warning
[20.10 backport] cli/config: prevent warning if HOME is not set
2021-02-12 10:27:24 +01:00
41b3ea7e47 Merge pull request #2960 from thaJeztah/20.10_backport_ignore_sigurg 2021-02-04 07:36:47 -08:00
d6eeeb6259 service rollback: always verify state
Prior to this change, progressbars would sometimes be hidden, and the function
would return early. In addition, the direction of the progressbars would sometimes
be "incrementing" (similar to "docker service update"), and sometimes be "decrementing"
(to indicate a "rollback" is being performed).

This fix makes sure that we always proceed with the "verifying" step, and now
prints a message _after_ the verifying stage was completed;

    $ docker service rollback foo
    foo
    overall progress: rolling back update: 5 out of 5 tasks
    1/5: running   [>                                                  ]
    2/5: starting  [===========>                                       ]
    3/5: starting  [===========>                                       ]
    4/5: running   [>                                                  ]
    5/5: running   [>                                                  ]
    verify: Service converged
    rollback: rollback completed

    $ docker service rollback foo
    foo
    overall progress: rolling back update: 1 out of 1 tasks
    1/1: running   [>                                                  ]
    verify: Service converged
    rollback: rollback completed

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 104469be0b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:55:51 +01:00
3e157d5293 docker service rollback: fix non-zero exit code in some cases
Before this change:
--------------------------------------------

    $ docker service create --replicas=1 --name foo -p 8080:80 nginx:alpine
    t33qvykv8y0zbz266rxynsbo3
    overall progress: 1 out of 1 tasks
    1/1: running   [==================================================>]
    verify: Service converged

    $ echo $?
    0

    $ docker service update --replicas=5 foo
    foo
    overall progress: 5 out of 5 tasks
    1/5: running   [==================================================>]
    2/5: running   [==================================================>]
    3/5: running   [==================================================>]
    4/5: running   [==================================================>]
    5/5: running   [==================================================>]
    verify: Service converged

    $ echo $?
    0

    $ docker service rollback foo
    foo
    rollback: manually requested rollback
    overall progress: rolling back update: 1 out of 1 tasks
    1/1: running   [>                                                  ]
    verify: Service converged

    $ echo $?
    0

    $ docker service rollback foo
    foo
    service rolled back: rollback completed

    $ echo $?
    1

After this change:
--------------------------------------------

    $ docker service create --replicas=1 --name foo -p 8080:80 nginx:alpine
    t33qvykv8y0zbz266rxynsbo3
    overall progress: 1 out of 1 tasks
    1/1: running   [==================================================>]
    verify: Service converged

    $ echo $?
    0

    $ docker service update --replicas=5 foo
    foo
    overall progress: 5 out of 5 tasks
    1/5: running   [==================================================>]
    2/5: running   [==================================================>]
    3/5: running   [==================================================>]
    4/5: running   [==================================================>]
    5/5: running   [==================================================>]
    verify: Waiting 1 seconds to verify that tasks are stable...

    $ echo $?
    0

    $ docker service rollback foo
    foo
    rollback: manually requested rollback
    overall progress: rolling back update: 1 out of 1 tasks
    1/1: running   [>                                                  ]
    verify: Service converged

    $ echo $?
    0

    $ docker service rollback foo
    foo
    service rolled back: rollback completed

    $ echo $?
    0

    $ docker service ps foo
    ID             NAME      IMAGE          NODE             DESIRED STATE   CURRENT STATE           ERROR     PORTS
    4dt4ms4c5qfb   foo.1     nginx:alpine   docker-desktop   Running         Running 2 minutes ago

Remaining issues with reconciliation
--------------------------------------------

Note that both before, and after this change, the command sometimes terminates
early, and does not wait for the service to reconcile; this is most apparent
when rolling back is scaling up (so more tasks are deployed);

    $ docker service rollback foo
    foo
    service rolled back: rollback completed

    $ docker service rollback foo
    foo
    rollback: manually requested rollback
    overall progress: rolling back update: 1 out of 5 tasks
    1/5: pending   [=================================>                 ]
    2/5: running   [>                                                  ]
    3/5: pending   [=================================>                 ]
    4/5: pending   [=================================>                 ]
    5/5: pending   [=================================>                 ]
    service rolled back: rollback completed

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit ce26a165b0)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:55:49 +01:00
1fdf84b8e9 fix --update-order and --rollback-order flags
Signed-off-by: Jim Lin <b04705003@ntu.edu.tw>
(cherry picked from commit 26a6a724aa)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:53:34 +01:00
376b99c6d6 Rename bin/md2man to bin/go-md2man
In the recent PR !2877, some code was added to check if md2man is
already installed in the build environment. This is to cater to the
needs of Linux distributions.

However it turns out that Linux distributions install md2man as
bin/go-md2man instead of bin/md2man, hence the PR !2877 doesn't help
much.

This commit fixes it by settling on using the binary name go-md2man.

For reference, here the file list of the package go-md2man in several
distributions:

- Debian: <https://packages.debian.org/sid/amd64/go-md2man/filelist>
- Ubuntu: <https://packages.ubuntu.com/hirsute/amd64/go-md2man/filelist>
- Fedora: <https://fedora.pkgs.org/31/fedora-x86_64/golang-github-cpuguy83-md2man-2.0.0-0.4.20190624gitf79a8a8.fc31.x86_64.rpm.html>
- ArchLinux: <https://www.archlinux.org/packages/community/x86_64/go-md2man/>

Signed-off-by: Arnaud Rebillout <elboulangero@gmail.com>
(cherry picked from commit 6e2607c6a6)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:50:45 +01:00
0de4e6e9a7 Fix reading context and dockerfile from stdin with BuildKit
Signed-off-by: Alexey Igrychev <alexey.igrychev@flant.com>
(cherry picked from commit fc9ca9a94a)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:48:19 +01:00
3c87f01b18 Ignore SIGURG on Linux.
In go1.14+, SIGURG is used by the runtime to handle preemtable system
calls.
In practice this signal caught *frequently*.

For reference:

https://go.googlesource.com/proposal/+/master/design/24543-non-cooperative-preemption.md
https://github.com/golang/go/issues/37942

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
(cherry picked from commit fff164c22e)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:44:39 +01:00
de40c2b172 Fix panic when failing to get DefaultAuthConfig
Commit f32731f902 fixed a potential panic
when an error was returned while trying to get existing credentials.

However, other code paths currently use the result of `GetDefaultAuthConfig()`
even in an error condition; this resulted in a panic, because a `nil` was
returned.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit c2820a7e3b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:42:06 +01:00
d513e46bfc cli/config: prevent warning if HOME is not set
commit c2626a8270 replaced the use of
github.com/docker/docker/pkg/homedir with Golang's os.UserHomeDir().

This change was partially reverted in 7a279af43d
to account for situations where `$HOME` is not set.

In  situations where no configuration file is present in `~/.config/`, the CLI
falls back to looking for the (deprecated) `~/.dockercfg` configuration file,
which was still using `os.UserHomeDir()`, which produces an error/warning if
`$HOME` is not set.

This patch introduces a helper function and a global variable to get the user's
home-directory. The global variable is used to prevent repeatedly looking up
the user's information (which, depending on the setup can be a costly operation).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit c85a37dbb4)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:38:27 +01:00
2b74b90efb Add docs and completion for docker node ls --filter node.label
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit f52a9e2fef)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:33:15 +01:00
05343b36a2 fix docker-run man page table formatting
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit c0b7b58134)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:33:10 +01:00
f90db254d7 docs: Fix wrong variable name
Signed-off-by: LeeDongGeon <secmatth1996@gmail.com>
(cherry picked from commit 852fe05991)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:33:06 +01:00
0dcfdde336 Removed format flag for inspect
Signed-off-by: Christopher Svensson <stoffus@stoffus.com>
(cherry picked from commit b04241d95a)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:33:01 +01:00
03cd1dc50a Added zsh completion for docker context subcommands
Signed-off-by: Christopher Svensson <stoffus@stoffus.com>
(cherry picked from commit 584c08e1fe)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:32:59 +01:00
42811a7eb9 docs: add redirect for old reference URL
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit a4fb01f957)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:32:54 +01:00
be966aa194 docs: fix typo in deprecated.md
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 697c3a5b48)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:32:49 +01:00
b22fe0fb14 deprecate blkio-weight options with cgroups v1
These options were deprecated and removed in the Linux kernel v5.0 and up in;

- f382fb0bce
- fb5772cbfe
- 23aa16489c

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit fb2ea098a9)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:32:44 +01:00
4eb050071e Update bash completion for fluentd --log-options
Signed-off-by: Harald Albers <github@albersweb.de>
(cherry picked from commit 5a252fb3ad)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:32:39 +01:00
08c4fdfa7a Add bash completion for dockerd --ip6tables
Signed-off-by: Harald Albers <github@albersweb.de>
(cherry picked from commit ba2fef9bcb)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:32:34 +01:00
6aa1b37c8d Add bash completion for docker run|create --pull
Signed-off-by: Harald Albers <github@albersweb.de>
(cherry picked from commit 8242fe1fcc)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:32:29 +01:00
e82920d76d Remove duplicate word in push.md
Signed-off-by: Roch Feuillade <roch.feuillade@pandobac.com>
(cherry picked from commit 69b5487e39)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:32:24 +01:00
82123939f7 Add bash completion for jobs
Signed-off-by: Harald Albers <github@albersweb.de>
(cherry picked from commit a4e86b5433)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-02-04 14:32:17 +01:00
48d30b5b32 Use golang.org/x/sys/execabs
On Windows, the os/exec.{Command,CommandContext,LookPath} functions
resolve command names that have neither path separators nor file extension
(e.g., "git") by first looking in the current working directory before
looking in the PATH environment variable.
Go maintainers intended to match cmd.exe's historical behavior.

However, this is pretty much never the intended behavior and as an abundance of precaution
this patch prevents that when executing commands.
Example of commands that docker.exe may execute: `git`, `docker-buildx` (or other cli plugin), `docker-credential-wincred`, `docker`.

Note that this was prompted by the [Go 1.15.7 security fixes](https://blog.golang.org/path-security), but unlike in `go.exe`,
the windows path lookups in docker are not in a code path allowing remote code execution, thus there is no security impact on docker.

Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit 8d199d5bba)
Signed-off-by: Tibor Vass <tibor@docker.com>
2021-01-28 22:27:59 +00:00
5941f4104a vendor docker, docker-credential-helpers and golang/sys for execabs package
Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit 7bef248765)
Signed-off-by: Tibor Vass <tibor@docker.com>
2021-01-28 22:27:59 +00:00
18f33b337d context: Add tarball e2e tests
Signed-off-by: Chris Crone <christopher.crone@docker.com>
2021-01-28 22:27:59 +00:00
9ecc69d17e context: Ensure context name is valid on import
Signed-off-by: Chris Crone <christopher.crone@docker.com>
2021-01-28 22:27:59 +00:00
6f49197cab context: Ensure import paths are valid
Signed-off-by: Chris Crone <christopher.crone@docker.com>
2021-01-28 22:27:59 +00:00
5678 changed files with 755297 additions and 526226 deletions

159
.circleci/config.yml Normal file
View File

@ -0,0 +1,159 @@
version: 2
jobs:
lint:
working_directory: /work
docker: [{image: 'docker:19.03-git'}]
environment:
DOCKER_BUILDKIT: 1
steps:
- checkout
- setup_remote_docker:
version: 19.03.12
reusable: true
exclusive: false
- run:
name: "Docker version"
command: docker version
- run:
name: "Docker info"
command: docker info
- run:
name: "Shellcheck - build image"
command: |
docker build --progress=plain -f dockerfiles/Dockerfile.shellcheck --tag cli-validator:$CIRCLE_BUILD_NUM .
- run:
name: "Shellcheck"
command: |
docker run --rm cli-validator:$CIRCLE_BUILD_NUM \
make shellcheck
- run:
name: "Lint - build image"
command: |
docker build --progress=plain -f dockerfiles/Dockerfile.lint --tag cli-linter:$CIRCLE_BUILD_NUM .
- run:
name: "Lint"
command: |
docker run --rm cli-linter:$CIRCLE_BUILD_NUM
cross:
working_directory: /work
docker: [{image: 'docker:19.03-git'}]
environment:
DOCKER_BUILDKIT: 1
parallelism: 3
steps:
- checkout
- setup_remote_docker:
version: 19.03.12
reusable: true
exclusive: false
- run:
name: "Docker version"
command: docker version
- run:
name: "Docker info"
command: docker info
- run:
name: "Cross - build image"
command: |
docker build --progress=plain -f dockerfiles/Dockerfile.cross --tag cli-builder:$CIRCLE_BUILD_NUM .
- run:
name: "Cross"
command: |
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:19.03-git'}]
environment:
DOCKER_BUILDKIT: 1
steps:
- checkout
- setup_remote_docker:
version: 19.03.12
reusable: true
exclusive: false
- run:
name: "Docker version"
command: docker version
- run:
name: "Docker info"
command: docker info
- run:
name: "Unit Test with Coverage - build image"
command: |
mkdir -p test-results/unit-tests
docker build --progress=plain -f dockerfiles/Dockerfile.dev --tag cli-builder:$CIRCLE_BUILD_NUM .
- run:
name: "Unit Test with Coverage"
command: |
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:19.03-git'}]
environment:
DOCKER_BUILDKIT: 1
steps:
- checkout
- setup_remote_docker:
version: 19.03.12
reusable: true
exclusive: false
- run:
name: "Docker version"
command: docker version
- run:
name: "Docker info"
command: docker info
- run:
name: "Validate - build image"
command: |
rm -f .dockerignore # include .git
docker build --progress=plain -f dockerfiles/Dockerfile.dev --tag cli-builder-with-git:$CIRCLE_BUILD_NUM .
- run:
name: "Validate Vendor, Docs, and Code Generation"
command: |
docker run --rm cli-builder-with-git:$CIRCLE_BUILD_NUM \
make ci-validate
no_output_timeout: 15m
workflows:
version: 2
ci:
jobs:
- lint
- cross
- test
- validate

View File

@ -1,14 +1,8 @@
/build/
/cmd/docker/winresources/versioninfo.json
/cmd/docker/winresources/*.syso
/man/man*/
/man/vendor/
/man/go.sum
/docs/yaml/
/docs/vendor/
/docs/go.sum
profile.out
# top-level go.mod is not meant to be checked in
/go.mod
/go.sum
.circleci
.dockerignore
.git
.github
.gitignore
appveyor.yml
build
/vndr.log

14
.gitattributes vendored
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

7
.github/CODEOWNERS vendored
View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,174 +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-24.04
outputs:
matrix: ${{ steps.platforms.outputs.matrix }}
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Create matrix
id: platforms
run: |
echo "matrix=$(docker buildx bake cross --print | jq -cr '.target."cross".platforms')" >>${GITHUB_OUTPUT}
-
name: Show matrix
run: |
echo ${{ steps.platforms.outputs.matrix }}
build:
runs-on: ubuntu-24.04
needs:
- prepare
strategy:
fail-fast: false
matrix:
target:
- binary
- dynbinary
platform: ${{ fromJson(needs.prepare.outputs.matrix) }}
use_glibc:
- ""
- glibc
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build
uses: docker/bake-action@v6
with:
targets: ${{ matrix.target }}
set: |
*.platform=${{ matrix.platform }}
env:
USE_GLIBC: ${{ matrix.use_glibc }}
-
name: Create tarball
working-directory: ./build
run: |
mkdir /tmp/out
platform=${{ matrix.platform }}
platformPair=${platform//\//-}
tar -cvzf "/tmp/out/docker-${platformPair}.tar.gz" .
if [ -z "${{ matrix.use_glibc }}" ]; then
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}" >> $GITHUB_ENV
else
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}-glibc" >> $GITHUB_ENV
fi
-
name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ env.ARTIFACT_NAME }}
path: /tmp/out/*
if-no-files-found: error
bin-image:
runs-on: ubuntu-24.04
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
steps:
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_CLIBIN_USERNAME }}
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: dockereng/cli-bin
tags: |
type=semver,pattern={{version}}
type=ref,event=branch
type=ref,event=pr
type=sha
-
name: Build and push image
uses: docker/bake-action@v6
with:
files: |
./docker-bake.hcl
cwd://${{ steps.meta.outputs.bake-file }}
targets: bin-image-cross
push: ${{ github.event_name != 'pull_request' }}
set: |
*.cache-from=type=gha,scope=bin-image
*.cache-to=type=gha,scope=bin-image,mode=max
prepare-plugins:
runs-on: ubuntu-24.04
outputs:
matrix: ${{ steps.platforms.outputs.matrix }}
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Create matrix
id: platforms
run: |
echo "matrix=$(docker buildx bake plugins-cross --print | jq -cr '.target."plugins-cross".platforms')" >>${GITHUB_OUTPUT}
-
name: Show matrix
run: |
echo ${{ steps.platforms.outputs.matrix }}
plugins:
runs-on: ubuntu-24.04
needs:
- prepare-plugins
strategy:
fail-fast: false
matrix:
platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }}
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build
uses: docker/bake-action@v6
with:
targets: plugins-cross
set: |
*.platform=${{ matrix.platform }}

80
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,80 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"
on:
# push:
# branches: [master]
# 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:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['go']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@ -1,79 +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-24.04
timeout-minutes: 10
env:
DISABLE_WARN_OUTSIDE_CONTAINER: '1'
permissions:
actions: read
contents: read
security-events: write
steps:
-
name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
# CodeQL 2.16.4's auto-build added support for multi-module repositories,
# and is trying to be smart by searching for modules in every directory,
# including vendor directories. If no module is found, it's creating one
# which is ... not what we want, so let's give it a "go.mod".
# see: https://github.com/docker/cli/pull/4944#issuecomment-2002034698
-
name: Create go.mod
run: |
ln -s vendor.mod go.mod
ln -s vendor.sum go.sum
-
name: Update Go
uses: actions/setup-go@v5
with:
go-version: "1.23.7"
-
name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: go
-
name: Autobuild
uses: github/codeql-action/autobuild@v3
-
name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:go"

View File

@ -1,80 +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-24.04
strategy:
fail-fast: false
matrix:
target:
- local
- connhelper-ssh
base:
- alpine
- debian
engine-version:
- 28 # latest
- 27 # latest - 1
- 26 # github actions default
- 23 # mirantis lts
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Update daemon.json
run: |
if [ ! -f /etc/docker/daemon.json ]; then
# ubuntu 24.04 runners no longer have a default daemon.json present
sudo mkdir -p /etc/docker/
echo '{"experimental": true}' | sudo tee /etc/docker/daemon.json
else
# but if there is one; let's patch it to keep other options that may be set.
sudo jq '.experimental = true' < /etc/docker/daemon.json > /tmp/docker.json
sudo mv /tmp/docker.json /etc/docker/daemon.json
fi
sudo cat /etc/docker/daemon.json
sudo service docker restart
docker version
docker info
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Run ${{ matrix.target }}
run: |
make -f docker.Makefile test-e2e-${{ matrix.target }}
env:
BASE_VARIANT: ${{ matrix.base }}
ENGINE_VERSION: ${{ matrix.engine-version }}
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
-
name: Send to Codecov
uses: codecov/codecov-action@v5
with:
files: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -1,83 +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-24.04
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Test
uses: docker/bake-action@v6
with:
targets: test-coverage
-
name: Send to Codecov
uses: codecov/codecov-action@v5
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-13 # macOS 13 on Intel
- macos-14 # macOS 14 on arm64 (Apple Silicon M1)
# - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history
steps:
-
name: Checkout
uses: actions/checkout@v4
with:
path: ${{ env.GOPATH }}/src/github.com/docker/cli
-
name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.23.7"
-
name: Test
run: |
go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/')
go tool cover -func=/tmp/coverage.txt
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
shell: bash
-
name: Send to Codecov
uses: codecov/codecov-action@v5
with:
files: /tmp/coverage.txt
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -1,88 +0,0 @@
name: validate-pr
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
on:
pull_request:
types: [opened, edited, labeled, unlabeled]
jobs:
check-area-label:
runs-on: ubuntu-24.04
timeout-minutes: 120 # guardrails timeout for the whole job
steps:
- name: Missing `area/` label
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
run: |
echo "::error::Every PR with an 'impact/*' label should also have an 'area/*' label"
exit 1
- name: OK
run: exit 0
check-changelog:
runs-on: ubuntu-24.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: 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-24.04
timeout-minutes: 120 # guardrails timeout for the whole job
env:
PR_TITLE: ${{ github.event.pull_request.title }}
steps:
# Backports or PR that target a release branch directly should mention the target branch in the title, for example:
# [X.Y backport] Some change that needs backporting to X.Y
# [X.Y] Change directly targeting the X.Y branch
- name: Check release branch
id: title_branch
run: |
# get the intended major version prefix ("[27.1 backport]" -> "27.") from the PR title.
[[ "$PR_TITLE" =~ ^\[([0-9]*\.)[^]]*\] ]] && branch="${BASH_REMATCH[1]}"
# get major version prefix from the release branch ("27.x -> "27.")
[[ "$GITHUB_BASE_REF" =~ ^([0-9]*\.) ]] && target_branch="${BASH_REMATCH[1]}" || target_branch="$GITHUB_BASE_REF"
if [[ "$target_branch" != "$branch" ]] && ! [[ "$GITHUB_BASE_REF" == "master" && "$branch" == "" ]]; then
echo "::error::PR is opened against the $GITHUB_BASE_REF branch, but its title suggests otherwise."
exit 1
fi

View File

@ -1,82 +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-24.04
strategy:
fail-fast: false
matrix:
target:
- lint
- shellcheck
- validate-vendor
- update-authors # ensure authors update target runs fine
steps:
-
name: Run
uses: docker/bake-action@v6
with:
targets: ${{ matrix.target }}
# check that the generated Markdown and the checked-in files match
validate-md:
runs-on: ubuntu-24.04
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Generate
shell: 'script --return --quiet --command "bash {0}"'
run: |
make -f docker.Makefile mddocs
-
name: Validate
run: |
if [[ $(git diff --stat) != '' ]]; then
echo 'fail: generated files do not match checked-in files'
git --no-pager diff
exit 1
fi
validate-make:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
target:
- yamldocs # ensure yamldocs target runs fine
- manpages # ensure manpages target runs fine
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Run
shell: 'script --return --quiet --command "bash {0}"'
run: |
make -f docker.Makefile ${{ matrix.target }}

16
.gitignore vendored
View File

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

View File

@ -1,205 +1,83 @@
linters:
enable:
- bodyclose
- copyloopvar # Detects places where loop variables are copied.
- depguard
- deadcode
- dogsled
- dupword # Detects duplicate words.
- durationcheck
- errchkjson
- gocritic # Metalinter; detects bugs, performance, and styling issues.
- gocyclo
- gofumpt # Detects whether code was gofumpt-ed.
- goimports
- gosec # Detects security problems.
- golint
- gosec
- gosimple
- govet
- ineffassign
- misspell # Detects commonly misspelled English words in comments.
- nakedret # Detects uses of naked returns.
- nilerr # Detects code that returns nil even if it checks that the error is not nil.
- nolintlint # Detects ill-formed or insufficient nolint directives.
- perfsprint # Detects fmt.Sprintf uses that can be replaced with a faster alternative.
- prealloc # Detects slice declarations that could potentially be pre-allocated.
- predeclared # Detects code that shadows one of Go's predeclared identifiers
- reassign
- revive # Metalinter; drop-in replacement for golint.
- interfacer
- lll
- megacheck
- misspell
- nakedret
- staticcheck
- stylecheck # Replacement for golint
- thelper # Detects test helpers without t.Helper().
- tparallel # Detects inappropriate usage of t.Parallel().
- structcheck
- typecheck
- unconvert # Detects unnecessary type conversions.
- unconvert
- unparam
- unused
- usestdlibvars
- usetesting # Reports uses of functions with replacement inside the testing package.
- wastedassign
- varcheck
disable:
- errcheck
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.23.7"
timeout: 5m
skip-dirs:
- cli/command/stack/kubernetes/api/openapi
- cli/command/stack/kubernetes/api/client
skip-files:
- cli/compose/schema/bindata.go
- .*generated.*
linters-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/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
gocyclo:
min-complexity: 16
gosec:
excludes:
- G104 # G104: Errors unhandled; (TODO: reduce unhandled errors, or explicitly ignore)
- G113 # G113: Potential uncontrolled memory consumption in Rat.SetString (CVE-2022-23772); (only affects go < 1.16.14. and go < 1.17.7)
- G115 # G115: integer overflow conversion; (TODO: verify these: https://github.com/docker/cli/issues/5584)
- 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")
govet:
enable:
- shadow
settings:
shadow:
strict: true
check-shadowing: false
lll:
line-length: 200
nakedret:
# Disallow naked returns if func has more lines of code than this setting.
# Default: 30
max-func-lines: 0
revive:
rules:
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block
- name: empty-block
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
- name: empty-lines
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing
- name: import-shadowing
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#line-length-limit
- name: line-length-limit
severity: warning
disabled: false
arguments: [200]
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver
- name: unused-receiver
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
- name: use-any
severity: warning
disabled: false
usetesting:
# FIXME(thaJeztah): Disable `os.Chdir()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
os-chdir: false
# FIXME(thaJeztah): Disable `context.Background()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
context-background: false
# FIXME(thaJeztah): Disable `context.TODO()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
context-todo: false
command: nakedret
pattern: ^(?P<path>.*?\\.go):(?P<line>\\d+)\\s*(?P<message>.*)$
issues:
# The default exclusion rules are a bit too permissive, so copying the relevant ones below
exclude-use-default: false
# This option has been defined when Go modules was not existed and when the
# golangci-lint core was different, this is not something we still recommend.
exclude-dirs-use-default: false
exclude:
- parameter .* always receives
exclude-files:
- cli/compose/schema/bindata.go
- .*generated.*
exclude-rules:
# We prefer to use an "exclude-list" so that new "default" exclusions are not
# automatically inherited. We can decide whether or not to follow upstream
# defaults when updating golang-ci-lint versions.
# Unfortunately, this means we have to copy the whole exclusion pattern, as
# (unlike the "include" option), the "exclude" option does not take exclusion
# ID's.
#
# These exclusion patterns are copied from the default excluses at:
# https://github.com/golangci/golangci-lint/blob/v1.44.0/pkg/config/issues.go#L10-L104
#
# The default list of exclusions can be found at:
# https://golangci-lint.run/usage/false-positives/#default-exclusions
# EXC0001
- text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv). is not checked"
# These are copied from the default exclude rules, except for "ineffective break statement"
# and GoDoc checks.
# https://github.com/golangci/golangci-lint/blob/0cc87df732aaf1d5ad9ce9ca538d38d916918b36/pkg/config/config.go#L36
- text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*printf?|os\\.(Un)?Setenv). is not checked"
linters:
- errcheck
# EXC0003
- text: "func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this"
linters:
- revive
# EXC0006
- text: "Use of unsafe calls should be audited"
- golint
- text: "G103: Use of unsafe calls should be audited"
linters:
- gosec
# EXC0007
- text: "Subprocess launch(ed with variable|ing should be audited)"
- text: "G104: Errors unhandled"
linters:
- gosec
# EXC0009
- text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
- text: "G204: Subprocess launch(ed with (variable|function call)|ing should be audited)"
linters:
- gosec
# EXC0010
- text: "Potential file inclusion via variable"
- text: "(G301|G302): (Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
linters:
- gosec
# TODO: make sure all packages have a description. Currently, there's 67 packages without.
- text: "package-comments: should have a package comment"
- text: "G304: Potential file inclusion via variable"
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"
- text: "(G201|G202): SQL string (formatting|concatenation)"
linters:
- stylecheck
# Allow "err" and "ok" vars to shadow existing declarations, otherwise we get too many false positives.
- text: '^shadow: declaration of "(err|ok)" shadows declaration'
linters:
- govet
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-issues-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0
- gosec

100
.mailmap
View File

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

165
AUTHORS
View File

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

View File

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

View File

@ -1,138 +0,0 @@
# syntax=docker/dockerfile:1
ARG BASE_VARIANT=alpine
ARG ALPINE_VERSION=3.21
ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.23.7
ARG XX_VERSION=1.6.1
ARG GOVERSIONINFO_VERSION=v1.4.1
ARG GOTESTSUM_VERSION=v1.12.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.20.1
ARG COMPOSE_VERSION=v2.32.4
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-base-alpine
ENV GOTOOLCHAIN=local
COPY --link --from=xx / /
RUN apk add --no-cache bash clang lld llvm file git
WORKDIR /go/src/github.com/docker/cli
FROM build-base-alpine AS build-alpine
ARG TARGETPLATFORM
# gcc is installed for libgcc only
RUN xx-apk add --no-cache musl-dev gcc
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-${BASE_DEBIAN_DISTRO} AS build-base-debian
ENV GOTOOLCHAIN=local
COPY --link --from=xx / /
RUN apt-get update && apt-get install --no-install-recommends -y bash clang lld llvm file
WORKDIR /go/src/github.com/docker/cli
FROM build-base-debian AS build-debian
ARG TARGETPLATFORM
RUN xx-apt-get install --no-install-recommends -y libc6-dev libgcc-12-dev pkgconf
FROM build-base-${BASE_VARIANT} AS goversioninfo
ARG GOVERSIONINFO_VERSION
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
GOBIN=/out GO111MODULE=on CGO_ENABLED=0 go install "github.com/josephspurrier/goversioninfo/cmd/goversioninfo@${GOVERSIONINFO_VERSION}"
FROM build-base-${BASE_VARIANT} AS gotestsum
ARG GOTESTSUM_VERSION
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
GOBIN=/out GO111MODULE=on CGO_ENABLED=0 go install "gotest.tools/gotestsum@${GOTESTSUM_VERSION}" \
&& /out/gotestsum --version
FROM build-${BASE_VARIANT} AS build
# GO_LINKMODE defines if static or dynamic binary should be produced
ARG GO_LINKMODE=static
# GO_BUILDTAGS defines additional build tags
ARG GO_BUILDTAGS
# GO_STRIP strips debugging symbols if set
ARG GO_STRIP
# CGO_ENABLED manually sets if cgo is used
ARG CGO_ENABLED
# VERSION sets the version for the produced binary
ARG VERSION
# PACKAGER_NAME sets the company that produced the windows binary
ARG PACKAGER_NAME
COPY --link --from=goversioninfo /out/goversioninfo /usr/bin/goversioninfo
RUN --mount=type=bind,target=.,ro \
--mount=type=cache,target=/root/.cache \
--mount=type=tmpfs,target=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/')
FROM scratch AS test-coverage
COPY --from=test /tmp/coverage.txt /coverage.txt
FROM build-${BASE_VARIANT} AS build-plugins
ARG GO_LINKMODE=static
ARG GO_BUILDTAGS
ARG GO_STRIP
ARG CGO_ENABLED
ARG VERSION
RUN --mount=ro --mount=type=cache,target=/root/.cache \
xx-go --wrap && \
TARGET=/out ./scripts/build/plugins e2e/cli-plugins/plugins/*
FROM build-base-alpine AS e2e-base-alpine
RUN apk add --no-cache build-base curl openssl openssh-client
FROM build-base-debian AS e2e-base-debian
RUN apt-get update && apt-get install -y build-essential curl openssl openssh-client
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
FROM docker/compose-bin:${COMPOSE_VERSION} AS compose
FROM e2e-base-${BASE_VARIANT} AS e2e
ARG NOTARY_VERSION=v0.6.1
ADD --chmod=0755 https://github.com/theupdateframework/notary/releases/download/${NOTARY_VERSION}/notary-Linux-amd64 /usr/local/bin/notary
COPY --link e2e/testdata/notary/root-ca.cert /usr/share/ca-certificates/notary.cert
RUN echo 'notary.cert' >> /etc/ca-certificates.conf && update-ca-certificates
COPY --link --from=gotestsum /out/gotestsum /usr/bin/gotestsum
COPY --link --from=build /out ./build/
COPY --link --from=build-plugins /out ./build/
COPY --link --from=buildx /buildx /usr/libexec/docker/cli-plugins/docker-buildx
COPY --link --from=compose /docker-compose /usr/libexec/docker/cli-plugins/docker-compose
COPY --link . .
ENV DOCKER_BUILDKIT=1
ENV PATH=/go/src/github.com/docker/cli/build:$PATH
CMD ./scripts/test/e2e/entry
FROM build-base-${BASE_VARIANT} AS dev
COPY --link . .
FROM scratch AS plugins
COPY --from=build-plugins /out .
FROM scratch AS bin-image-linux
COPY --from=build /out/docker /docker
FROM scratch AS bin-image-darwin
COPY --from=build /out/docker /docker
FROM scratch AS bin-image-windows
COPY --from=build /out/docker /docker.exe
FROM bin-image-${TARGETOS} AS bin-image
FROM scratch AS binary
COPY --from=build /out .

47
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,47 @@
pipeline {
agent {
label "linux && x86_64"
}
options {
timeout(time: 60, unit: 'MINUTES')
}
stages {
stage("Docker info") {
steps {
sh "docker version"
sh "docker info"
}
}
stage("e2e (non-experimental) - stable engine") {
steps {
sh "E2E_UNIQUE_ID=clie2e${BUILD_NUMBER} \
IMAGE_TAG=clie2e${BUILD_NUMBER} \
make -f docker.Makefile test-e2e-non-experimental"
}
}
stage("e2e (non-experimental) - 18.09 engine") {
steps {
sh "E2E_ENGINE_VERSION=18.09-dind \
E2E_UNIQUE_ID=clie2e${BUILD_NUMBER} \
IMAGE_TAG=clie2e${BUILD_NUMBER} \
make -f docker.Makefile test-e2e-non-experimental"
}
}
stage("e2e (experimental)") {
steps {
sh "E2E_UNIQUE_ID=clie2e${BUILD_NUMBER} \
IMAGE_TAG=clie2e${BUILD_NUMBER} \
make -f docker.Makefile test-e2e-experimental"
}
}
stage("e2e (ssh connhelper)") {
steps {
sh "E2E_UNIQUE_ID=clie2e${BUILD_NUMBER} \
IMAGE_TAG=clie2e${BUILD_NUMBER} \
make -f docker.Makefile test-e2e-connhelper-ssh"
}
}
}
}

View File

@ -24,7 +24,6 @@
people = [
"albers",
"cpuguy83",
"rumpl",
"silvin-lubecki",
"stevvooe",
"thajeztah",
@ -47,11 +46,8 @@
# - close an issue or pull request when it's inappropriate or off-topic
people = [
"bsousaa",
"neersighted",
"programmerq",
"sam-thibault",
"vvoland"
"thajeztah"
]
[Org.Alumni]
@ -82,11 +78,6 @@
Email = "github@albersweb.de"
GitHub = "albers"
[people.bsousaa]
Name = "Bruno de Sousa"
Email = "bruno.sousa@docker.com"
GitHub = "bsousaa"
[people.cpuguy83]
Name = "Brian Goff"
Email = "cpuguy83@gmail.com"
@ -97,26 +88,11 @@
Email = "dnephin@gmail.com"
GitHub = "dnephin"
[people.neersighted]
Name = "Bjorn Neergaard"
Email = "bneergaard@mirantis.com"
GitHub = "neersighted"
[people.programmerq]
Name = "Jeff Anderson"
Email = "jeff@docker.com"
GitHub = "programmerq"
[people.rumpl]
Name = "Djordje Lukic"
Email = "djordje.lukic@docker.com"
GitHub = "rumpl"
[people.sam-thibault]
Name = "Sam Thibault"
Email = "sam.thibault@docker.com"
GitHub = "sam-thibault"
[people.silvin-lubecki]
Name = "Silvin Lubecki"
Email = "silvin.lubecki@docker.com"
@ -152,8 +128,3 @@
Email = "vieux@docker.com"
GitHub = "vieux"
[people.vvoland]
Name = "Paweł Gronowski"
Email = "pawel.gronowski@docker.com"
GitHub = "vvoland"

145
Makefile
View File

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

@ -14,6 +14,6 @@ United States and other governments.
It is your responsibility to ensure that your use and/or transfer does not
violate applicable laws.
For more information, see https://www.bis.doc.gov
For more information, please see https://www.bis.doc.gov
See also https://www.apache.org/dev/crypto.html and/or seek legal counsel.

View File

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

View File

@ -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.

View File

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

View File

@ -1 +1 @@
28.0.0-dev
20.10.0-dev

23
appveyor.yml Normal file
View File

@ -0,0 +1,23 @@
version: "{build}"
clone_folder: c:\gopath\src\github.com\docker\cli
environment:
GOPATH: c:\gopath
GOVERSION: 1.13.15
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

View File

@ -5,31 +5,31 @@ import (
"fmt"
"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/command"
"github.com/spf13/cobra"
)
func main() {
plugin.Run(func(dockerCLI command.Cli) *cobra.Command {
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
goodbye := &cobra.Command{
Use: "goodbye",
Short: "Say Goodbye instead of Hello",
Run: func(cmd *cobra.Command, _ []string) {
_, _ = fmt.Fprintln(dockerCLI.Out(), "Goodbye World!")
fmt.Fprintln(dockerCli.Out(), "Goodbye World!")
},
}
apiversion := &cobra.Command{
Use: "apiversion",
Short: "Print the API version of the server",
RunE: func(_ *cobra.Command, _ []string) error {
apiClient := dockerCLI.Client()
ping, err := apiClient.Ping(context.Background())
cli := dockerCli.Client()
ping, err := cli.Ping(context.Background())
if err != nil {
return err
}
_, _ = fmt.Println(ping.APIVersion)
fmt.Println(ping.APIVersion)
return nil
},
}
@ -38,15 +38,15 @@ func main() {
Use: "exitstatus2",
Short: "Exit with status 2",
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)
return nil
},
}
var (
who, optContext string
preRun, debug bool
who, context string
preRun, debug bool
)
cmd := &cobra.Command{
Use: "helloworld",
@ -56,33 +56,33 @@ func main() {
return err
}
if preRun {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Plugin PersistentPreRunE called")
fmt.Fprintf(dockerCli.Err(), "Plugin PersistentPreRunE called")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if debug {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Plugin debug mode enabled")
fmt.Fprintf(dockerCli.Err(), "Plugin debug mode enabled")
}
switch optContext {
switch context {
case "Christmas":
_, _ = fmt.Fprintln(dockerCLI.Out(), "Merry Christmas!")
fmt.Fprintf(dockerCli.Out(), "Merry Christmas!\n")
return nil
case "":
// nothing
}
if who == "" {
who, _ = dockerCLI.ConfigFile().PluginConfig("helloworld", "who")
who, _ = dockerCli.ConfigFile().PluginConfig("helloworld", "who")
}
if who == "" {
who = "World"
}
_, _ = fmt.Fprintln(dockerCLI.Out(), "Hello", who)
dockerCLI.ConfigFile().SetPluginConfig("helloworld", "lastwho", who)
return dockerCLI.ConfigFile().Save()
fmt.Fprintf(dockerCli.Out(), "Hello %s!\n", who)
dockerCli.ConfigFile().SetPluginConfig("helloworld", "lastwho", who)
return dockerCli.ConfigFile().Save()
},
}
@ -92,12 +92,12 @@ func main() {
// These are intended to deliberately clash with the CLIs own top
// level arguments.
flags.BoolVarP(&debug, "debug", "D", false, "Enable debug")
flags.StringVarP(&optContext, "context", "c", "", "Is it Christmas?")
flags.StringVarP(&context, "context", "c", "", "Is it Christmas?")
cmd.AddCommand(goodbye, apiversion, exitStatus2)
return cmd
},
metadata.Metadata{
manager.Metadata{
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: "testing",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,7 @@
package manager
import (
"os/exec"
"github.com/docker/cli/cli-plugins/metadata"
exec "golang.org/x/sys/execabs"
)
// Candidate represents a possible plugin candidate, for mocking purposes
@ -21,5 +19,5 @@ func (c *candidate) Path() string {
}
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()
}

View File

@ -6,7 +6,6 @@ import (
"strings"
"testing"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
@ -31,10 +30,10 @@ func (c *fakeCandidate) Metadata() ([]byte, error) {
func TestValidateCandidate(t *testing.T) {
const (
goodPluginName = metadata.NamePrefix + "goodplugin"
goodPluginName = NamePrefix + "goodplugin"
builtinName = metadata.NamePrefix + "builtin"
builtinAlias = metadata.NamePrefix + "alias"
builtinName = NamePrefix + "builtin"
builtinAlias = NamePrefix + "alias"
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
@ -44,9 +43,9 @@ func TestValidateCandidate(t *testing.T) {
fakeroot := &cobra.Command{Use: "docker"}
fakeroot.AddCommand(&cobra.Command{
Use: strings.TrimPrefix(builtinName, metadata.NamePrefix),
Use: strings.TrimPrefix(builtinName, NamePrefix),
Aliases: []string{
strings.TrimPrefix(builtinAlias, metadata.NamePrefix),
strings.TrimPrefix(builtinAlias, NamePrefix),
},
})
@ -60,7 +59,7 @@ func TestValidateCandidate(t *testing.T) {
}{
/* Each failing one of the tests */
{name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
{name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix)},
{name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", NamePrefix)},
{name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
{name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`},
{name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`},
@ -75,17 +74,16 @@ func TestValidateCandidate(t *testing.T) {
{name: "experimental + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental}},
} {
t.Run(tc.name, func(t *testing.T) {
p, err := newPlugin(tc.c, fakeroot.Commands())
switch {
case tc.err != "":
p, err := newPlugin(tc.c, fakeroot)
if tc.err != "" {
assert.ErrorContains(t, err, tc.err)
case tc.invalid != "":
} else if tc.invalid != "" {
assert.NilError(t, err)
assert.Assert(t, cmp.ErrorType(p.Err, reflect.TypeOf(&pluginError{})))
assert.ErrorContains(t, p.Err, tc.invalid)
default:
} else {
assert.NilError(t, err)
assert.Equal(t, metadata.NamePrefix+p.Name, goodPluginName)
assert.Equal(t, NamePrefix+p.Name, goodPluginName)
assert.Equal(t, p.SchemaVersion, "0.1.0")
assert.Equal(t, p.Vendor, "e2e-testing")
}

View File

@ -1,77 +1,60 @@
package manager
import (
"fmt"
"os"
"sync"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/command"
"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
// plugin. The command stubs will have several annotations added, see
// `CommandAnnotationPlugin*`.
func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (err error) {
pluginCommandStubsOnce.Do(func() {
var plugins []Plugin
plugins, err = ListPlugins(dockerCLI, rootCmd)
if err != nil {
return
func AddPluginCommandStubs(dockerCli command.Cli, cmd *cobra.Command) error {
plugins, err := ListPlugins(dockerCli, cmd)
if err != nil {
return err
}
for _, p := range plugins {
vendor := p.Vendor
if vendor == "" {
vendor = "unknown"
}
for _, p := range plugins {
vendor := p.Vendor
if vendor == "" {
vendor = "unknown"
}
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,
Run: func(_ *cobra.Command, _ []string) {},
Annotations: annotations,
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
flags := rootCmd.PersistentFlags()
flags.SetOutput(nil)
perr := flags.Parse(args)
if perr != nil {
return err
}
if flags.Changed("help") {
cmd.HelpFunc()(rootCmd, args)
return nil
}
return fmt.Errorf("docker: 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}
cargs = append(cargs, args...)
cargs = append(cargs, toComplete)
os.Args = cargs
runCommand, runErr := PluginRunCommand(dockerCLI, p.Name, cmd)
if runErr != nil {
return nil, cobra.ShellCompDirectiveError
}
runErr = runCommand.Run()
if runErr == nil {
os.Exit(0) // plugin already rendered complete data
}
return nil, cobra.ShellCompDirectiveError
},
})
annotations := map[string]string{
CommandAnnotationPlugin: "true",
CommandAnnotationPluginVendor: vendor,
CommandAnnotationPluginVersion: p.Version,
}
})
return err
if p.Err != nil {
annotations[CommandAnnotationPluginInvalid] = p.Err.Error()
}
cmd.AddCommand(&cobra.Command{
Use: p.Name,
Short: p.ShortDescription,
Run: func(_ *cobra.Command, _ []string) {},
Annotations: annotations,
})
}
return nil
}

View File

@ -1,26 +0,0 @@
package manager
import (
"testing"
"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)
}

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.22
package manager
import (
"fmt"
"github.com/pkg/errors"
)
// pluginError is set as Plugin.Err by NewPlugin if the plugin
@ -39,16 +36,13 @@ func (e *pluginError) MarshalText() (text []byte, err error) {
}
// wrapAsPluginError wraps an error in a pluginError with an
// additional message.
// additional message, analogous to errors.Wrapf.
func wrapAsPluginError(err error, msg string) error {
if err == nil {
return nil
}
return &pluginError{cause: fmt.Errorf("%s: %w", msg, err)}
return &pluginError{cause: errors.Wrap(err, msg)}
}
// NewPluginError creates a new pluginError, analogous to
// errors.Errorf.
func NewPluginError(msg string, args ...any) error {
return &pluginError{cause: fmt.Errorf(msg, args...)}
func NewPluginError(msg string, args ...interface{}) error {
return &pluginError{cause: errors.Errorf(msg, args...)}
}

View File

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

View File

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

View File

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

View File

@ -1,40 +1,29 @@
package manager
import (
"context"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/fvbommel/sortorder"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
exec "golang.org/x/sys/execabs"
)
const (
// ReexecEnvvar is the name of an ennvar which is set to the command
// used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI.
ReexecEnvvar = metadata.ReexecEnvvar
// ResourceAttributesEnvvar is the name of the envvar that includes additional
// resource attributes for OTEL.
//
// Deprecated: The "OTEL_RESOURCE_ATTRIBUTES" env-var is part of the OpenTelemetry specification; users should define their own const for this. This const will be removed in the next release.
ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES"
)
// ReexecEnvvar is the name of an ennvar which is set to the command
// used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI.
const ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
// errPluginNotFound is the error returned when a plugin could not be found.
type errPluginNotFound string
func (errPluginNotFound) NotFound() {}
func (e errPluginNotFound) NotFound() {}
func (e errPluginNotFound) Error() string {
return "Error: No such CLI plugin: " + string(e)
@ -51,20 +40,10 @@ func IsNotFound(err error) bool {
return ok
}
// getPluginDirs returns the platform-specific locations to search for plugins
// in order of preference.
//
// Plugin-discovery is performed in the following order of preference:
//
// 1. The "cli-plugins" directory inside the CLIs [config.Path] (usually "~/.docker/cli-plugins").
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs].
// 3. Platform-specific defaultSystemPluginDirs.
//
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
func getPluginDirs(cfg *configfile.ConfigFile) ([]string, error) {
func getPluginDirs(dockerCli command.Cli) ([]string, error) {
var pluginDirs []string
if cfg != nil {
if cfg := dockerCli.ConfigFile(); cfg != nil {
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
}
pluginDir, err := config.Path("cli-plugins")
@ -77,15 +56,13 @@ func getPluginDirs(cfg *configfile.ConfigFile) ([]string, error) {
return pluginDirs, nil
}
func addPluginCandidatesFromDir(res map[string][]string, d string) {
dentries, err := os.ReadDir(d)
// Silently ignore any directories which we cannot list (e.g. due to
// permissions or anything else) or which is not a directory
func addPluginCandidatesFromDir(res map[string][]string, d string) error {
dentries, err := ioutil.ReadDir(d)
if err != nil {
return
return err
}
for _, dentry := range dentries {
switch dentry.Type() & os.ModeType {
switch dentry.Mode() & os.ModeType {
case 0, os.ModeSymlink:
// Regular file or symlink, keep going
default:
@ -93,92 +70,66 @@ func addPluginCandidatesFromDir(res map[string][]string, d string) {
continue
}
name := dentry.Name()
if !strings.HasPrefix(name, metadata.NamePrefix) {
if !strings.HasPrefix(name, NamePrefix) {
continue
}
name = strings.TrimPrefix(name, metadata.NamePrefix)
name = strings.TrimPrefix(name, NamePrefix)
var err error
if name, err = trimExeSuffix(name); err != nil {
continue
}
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.
func listPluginCandidates(dirs []string) map[string][]string {
func listPluginCandidates(dirs []string) (map[string][]string, error) {
result := make(map[string][]string)
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, err := getPluginDirs(dockerCLI.ConfigFile())
// ListPlugins produces a list of the plugins available on the system
func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
pluginDirs, err := getPluginDirs(dockerCli)
if err != nil {
return nil, err
}
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 {
candidates, err := listPluginCandidates(pluginDirs)
if err != nil {
return nil, err
}
var plugins []Plugin
for _, paths := range candidates {
if len(paths) == 0 {
return nil, errPluginNotFound(name)
continue
}
c := &candidate{paths[0]}
p, err := newPlugin(c, rootcmd.Commands())
p, err := newPlugin(c, rootcmd)
if err != nil {
return nil, err
}
if !IsNotFound(p.Err) {
p.ShadowedPaths = paths[1:]
plugins = append(plugins, p)
}
return &p, nil
}
return nil, errPluginNotFound(name)
}
// ListPlugins produces a list of the plugins available on the system
func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, error) {
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
if err != nil {
return nil, err
}
candidates := listPluginCandidates(pluginDirs)
var plugins []Plugin
var mu sync.Mutex
eg, _ := errgroup.WithContext(context.TODO())
cmds := rootcmd.Commands()
for _, paths := range candidates {
func(paths []string) {
eg.Go(func() error {
if len(paths) == 0 {
return nil
}
c := &candidate{paths[0]}
p, err := newPlugin(c, cmds)
if err != nil {
return err
}
if !IsNotFound(p.Err) {
p.ShadowedPaths = paths[1:]
mu.Lock()
defer mu.Unlock()
plugins = append(plugins, p)
}
return nil
})
}(paths)
}
if err := eg.Wait(); err != nil {
return nil, err
}
sort.Slice(plugins, func(i, j int) bool {
@ -191,7 +142,7 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e
// 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 conficts.
// 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
// have been provided by cobra to our caller. This is because
// they lack e.g. global options which we must propagate here.
@ -201,8 +152,8 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
// fallback to their "invalid" command path.
return nil, errPluginNotFound(name)
}
exename := addExeSuffix(metadata.NamePrefix + name)
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
exename := addExeSuffix(NamePrefix + name)
pluginDirs, err := getPluginDirs(dockerCli)
if err != nil {
return nil, err
}
@ -219,7 +170,7 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
}
c := &candidate{path: path}
plugin, err := newPlugin(c, rootcmd.Commands())
plugin, err := newPlugin(c, rootcmd)
if err != nil {
return nil, err
}
@ -227,8 +178,7 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
// TODO: why are we not returning plugin.Err?
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.
// See: - https://github.com/golang/go/issues/10338
// - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab
@ -238,15 +188,10 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = append(cmd.Environ(), metadata.ReexecEnvvar+"="+os.Args[0])
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
return cmd, nil
}
return nil, errPluginNotFound(name)
}
// IsPluginCommand checks if the given cmd is a plugin-stub.
func IsPluginCommand(cmd *cobra.Command) bool {
return cmd.Annotations[metadata.CommandAnnotationPlugin] == "true"
}

View File

@ -46,12 +46,13 @@ func TestListPluginCandidates(t *testing.T) {
)
defer dir.Remove()
dirs := make([]string, 0, 6)
var dirs []string
for _, d := range []string{"plugins1", "nonexistent", "plugins2", "plugins3", "plugins4", "plugins5"} {
dirs = append(dirs, dir.Join(d))
}
candidates := listPluginCandidates(dirs)
candidates, err := listPluginCandidates(dirs)
assert.NilError(t, err)
exp := map[string][]string{
"plugin1": {
dir.Join("plugins1", "docker-plugin1"),
@ -81,60 +82,14 @@ func TestListPluginCandidates(t *testing.T) {
assert.DeepEqual(t, candidates, exp)
}
// 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 TestListPluginCandidatesInaccesibleDir(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, 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)),
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0777)),
fs.WithFile("docker-aaa", `
#!/bin/sh
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0777)),
)
defer dir.Remove()
@ -171,7 +126,7 @@ func TestGetPluginDirs(t *testing.T) {
expected := append([]string{pluginDir}, defaultSystemPluginDirs...)
var pluginDirs []string
pluginDirs, err = getPluginDirs(cli.ConfigFile())
pluginDirs, err = getPluginDirs(cli)
assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))
assert.NilError(t, err)
@ -182,7 +137,7 @@ func TestGetPluginDirs(t *testing.T) {
cli.SetConfigFile(&configfile.ConfigFile{
CLIPluginsExtraDirs: extras,
})
pluginDirs, err = getPluginDirs(cli.ConfigFile())
pluginDirs, err = getPluginDirs(cli)
assert.DeepEqual(t, expected, pluginDirs)
assert.NilError(t, err)
}

View File

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

View File

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

View File

@ -1,23 +1,28 @@
package manager
import (
"github.com/docker/cli/cli-plugins/metadata"
)
const (
// NamePrefix is the prefix required on all plugin binary names
NamePrefix = metadata.NamePrefix
NamePrefix = "docker-"
// MetadataSubcommandName is the name of the plugin subcommand
// which must be supported by every plugin and returns the
// plugin metadata.
MetadataSubcommandName = metadata.MetadataSubcommandName
// HookSubcommandName is the name of the plugin subcommand
// which must be implemented by plugins declaring support
// for hooks in their metadata.
HookSubcommandName = metadata.HookSubcommandName
MetadataSubcommandName = "docker-cli-plugin-metadata"
)
// Metadata provided by the plugin.
type Metadata = metadata.Metadata
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.
// Deprecated: experimental features are now always enabled in the CLI
Experimental bool `json:",omitempty"`
}

View File

@ -1,25 +1,22 @@
package manager
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
var pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
var (
pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
)
// Plugin represents a potential plugin with all it's metadata.
type Plugin struct {
metadata.Metadata
Metadata
Name string `json:",omitempty"`
Path string `json:",omitempty"`
@ -36,7 +33,9 @@ type Plugin struct {
// is set, and is always a `pluginError`, but the `Plugin` is still
// returned with no error. An error is only returned due to a
// non-recoverable error.
func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
//
// nolint: gocyclo
func newPlugin(c Candidate, rootcmd *cobra.Command) (Plugin, error) {
path := c.Path()
if path == "" {
return Plugin{}, errors.New("plugin candidate path cannot be empty")
@ -46,18 +45,18 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
// which would fail here, so there are all real errors.
fullname := filepath.Base(path)
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
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) {
return Plugin{}, fmt.Errorf("plugin candidate %q: does not have %q prefix", path, metadata.NamePrefix)
if !strings.HasPrefix(fullname, NamePrefix) {
return Plugin{}, errors.Errorf("plugin candidate %q: does not have %q prefix", path, NamePrefix)
}
p := Plugin{
Name: strings.TrimPrefix(fullname, metadata.NamePrefix),
Name: strings.TrimPrefix(fullname, NamePrefix),
Path: path,
}
@ -67,20 +66,22 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
return p, nil
}
for _, cmd := range cmds {
// Ignore conflicts with commands which are
// just plugin stubs (i.e. from a previous
// call to AddPluginCommandStubs).
if IsPluginCommand(cmd) {
continue
}
if cmd.Name() == p.Name {
p.Err = NewPluginError("plugin %q duplicates builtin command", p.Name)
return p, nil
}
if cmd.HasAlias(p.Name) {
p.Err = NewPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name())
return p, nil
if rootcmd != nil {
for _, cmd := range rootcmd.Commands() {
// Ignore conflicts with commands which are
// just plugin stubs (i.e. from a previous
// call to AddPluginCommandStubs).
if p := cmd.Annotations[CommandAnnotationPlugin]; p == "true" {
continue
}
if cmd.Name() == p.Name {
p.Err = NewPluginError("plugin %q duplicates builtin command", p.Name)
return p, nil
}
if cmd.HasAlias(p.Name) {
p.Err = NewPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name())
return p, nil
}
}
}
@ -105,22 +106,3 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
}
return p, nil
}
// RunHook executes the plugin's hooks command
// and returns its unprocessed output.
func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte, error) {
hDataBytes, err := json.Marshal(hookData)
if err != nil {
return nil, wrapAsPluginError(err, "failed to marshall hook data")
}
pCmd := exec.CommandContext(ctx, p.Path, p.Name, 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])
hookCmdOutput, err := pCmd.Output()
if err != nil {
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")
}
return hookCmdOutput, nil
}

View File

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

View File

@ -1,16 +1,22 @@
package manager
import (
"fmt"
"path/filepath"
"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) {
ext := filepath.Ext(s)
if ext == "" || !strings.EqualFold(ext, ".exe") {
return "", fmt.Errorf("path %q lacks required file extension (.exe)", s)
if ext == "" {
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
}

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
}

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"
)

View File

@ -1,36 +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"`
}

View File

@ -1,73 +1,44 @@
package plugin
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"sync"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli-plugins/socket"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/connhelper"
"github.com/docker/cli/cli/debug"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
)
// PersistentPreRunE must be called by any plugin command (or
// subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins
// which do not make use of `PersistentPreRun*` do not need to call
// this (although it remains safe to do so). Plugins are recommended
// to use `PersistentPreRunE` to enable the error to be
// to use `PersistenPreRunE` to enable the error to be
// returned. Should not be called outside of a command's
// PersistentPreRunE hook and must not be run unless Run has been
// called.
var PersistentPreRunE func(*cobra.Command, []string) error
// RunPlugin executes the specified plugin command
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) error {
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) error {
tcmd := newPluginCommand(dockerCli, plugin, meta)
var persistentPreRunOnce sync.Once
PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
var retErr error
PersistentPreRunE = func(_ *cobra.Command, _ []string) error {
var err error
persistentPreRunOnce.Do(func() {
ctx, cancel := context.WithCancel(cmd.Context())
cmd.SetContext(ctx)
// Set up the context to cancel based on signalling via CLI socket.
socket.ConnectAndWait(cancel)
var opts []command.CLIOption
var opts []command.InitializeOpt
if os.Getenv("DOCKER_CLI_PLUGIN_USE_DIAL_STDIO") != "" {
opts = append(opts, withPluginClientConn(plugin.Name()))
}
opts = append(opts, command.WithEnableGlobalMeterProvider(), command.WithEnableGlobalTracerProvider())
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
}
err = tcmd.Initialize(opts...)
})
return retErr
return err
}
cmd, args, err := tcmd.HandleGlobalFlags()
@ -81,9 +52,7 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
}
// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) {
otel.SetErrorHandler(debug.OTELErrorHandler)
func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
dockerCli, err := command.NewDockerCli()
if err != nil {
fmt.Fprintln(os.Stderr, err)
@ -93,25 +62,26 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) {
plugin := makeCmd(dockerCli)
if err := RunPlugin(dockerCli, plugin, meta); err != nil {
var stErr cli.StatusError
if errors.As(err, &stErr) {
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
// 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.
stErr.StatusCode = 1
if sterr.StatusCode == 0 {
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)
}
}
func withPluginClientConn(name string) command.CLIOption {
func withPluginClientConn(name string) command.InitializeOpt {
return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) {
cmd := "docker"
if x := os.Getenv(metadata.ReexecEnvvar); x != "" {
if x := os.Getenv(manager.ReexecEnvvar); x != "" {
cmd = x
}
var flags []string
@ -140,9 +110,9 @@ func withPluginClientConn(name string) command.CLIOption {
})
}
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()
fullname := metadata.NamePrefix + name
fullname := manager.NamePrefix + name
cmd := &cobra.Command{
Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name),
@ -155,17 +125,10 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
},
TraverseChildren: true,
DisableFlagsInUseLine: true,
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: false,
HiddenDefaultCmd: true,
DisableDescriptions: os.Getenv("DOCKER_CLI_DISABLE_COMPLETION_DESCRIPTION") != "",
},
}
opts, _ := cli.SetupPluginRootCommand(cmd)
opts, flags := cli.SetupPluginRootCommand(cmd)
cmd.SetIn(dockerCli.In())
cmd.SetOut(dockerCli.Out())
cmd.SetErr(dockerCli.Err())
cmd.AddCommand(
plugin,
@ -174,15 +137,15 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
cli.DisableFlagsInUseLine(cmd)
return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags())
return cli.NewTopLevelCommand(cmd, dockerCli, opts, flags)
}
func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra.Command {
func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
if meta.ShortDescription == "" {
meta.ShortDescription = plugin.Short
}
cmd := &cobra.Command{
Use: metadata.MetadataSubcommandName,
Use: manager.MetadataSubcommandName,
Hidden: true,
// Suppress the global/parent PersistentPreRunE, which
// needlessly initializes the client and tries to
@ -197,11 +160,3 @@ func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra
}
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
}

View File

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

View File

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

View File

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

View File

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

View File

@ -3,13 +3,12 @@ package cli
import (
"fmt"
"os"
"sort"
"strings"
"github.com/docker/cli/cli-plugins/metadata"
pluginmanager "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/command"
cliconfig "github.com/docker/cli/cli/config"
cliflags "github.com/docker/cli/cli/flags"
"github.com/fvbommel/sortorder"
"github.com/moby/term"
"github.com/morikuni/aec"
"github.com/pkg/errors"
@ -19,22 +18,19 @@ import (
// setupCommonRootCommand contains the setup common to
// SetupRootCommand and SetupPluginRootCommand.
func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *cobra.Command) {
func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet, *cobra.Command) {
opts := cliflags.NewClientOptions()
opts.InstallFlags(rootCmd.Flags())
flags := rootCmd.Flags()
flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files")
opts.Common.InstallFlags(flags)
cobra.AddTemplateFunc("add", func(a, b int) int { return a + b })
cobra.AddTemplateFunc("hasAliases", hasAliases)
cobra.AddTemplateFunc("hasSubCommands", hasSubCommands)
cobra.AddTemplateFunc("hasTopCommands", hasTopCommands)
cobra.AddTemplateFunc("hasManagementSubCommands", hasManagementSubCommands)
cobra.AddTemplateFunc("hasSwarmSubCommands", hasSwarmSubCommands)
cobra.AddTemplateFunc("hasInvalidPlugins", hasInvalidPlugins)
cobra.AddTemplateFunc("topCommands", topCommands)
cobra.AddTemplateFunc("commandAliases", commandAliases)
cobra.AddTemplateFunc("operationSubCommands", operationSubCommands)
cobra.AddTemplateFunc("managementSubCommands", managementSubCommands)
cobra.AddTemplateFunc("orchestratorSubCommands", orchestratorSubCommands)
cobra.AddTemplateFunc("invalidPlugins", invalidPlugins)
cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages)
cobra.AddTemplateFunc("vendorAndVersion", vendorAndVersion)
@ -51,28 +47,28 @@ func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *c
rootCmd.SetHelpCommand(helpCommand)
rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage")
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "use --help")
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")
rootCmd.PersistentFlags().Lookup("help").Hidden = true
rootCmd.Annotations = map[string]string{
"additionalHelp": "For more help on how to use Docker, head to https://docs.docker.com/go/guides/",
"docs.code-delimiter": `"`, // https://github.com/docker/cli-docs-tool/blob/77abede22166eaea4af7335096bdcedd043f5b19/annotation/annotation.go#L20-L22
}
rootCmd.Annotations = map[string]string{"additionalHelp": "To get more help with docker, check out our guides at https://docs.docker.com/go/guides/"}
return opts, helpCommand
return opts, flags, helpCommand
}
// SetupRootCommand sets default usage, help, and error handling for the
// root command.
func SetupRootCommand(rootCmd *cobra.Command) (opts *cliflags.ClientOptions, helpCmd *cobra.Command) {
func SetupRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet, *cobra.Command) {
opts, flags, helpCmd := setupCommonRootCommand(rootCmd)
rootCmd.SetVersionTemplate("Docker version {{.Version}}\n")
return setupCommonRootCommand(rootCmd)
return opts, flags, helpCmd
}
// SetupPluginRootCommand sets default usage, help and error handling for a plugin root command.
func SetupPluginRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) {
opts, _ := setupCommonRootCommand(rootCmd)
return opts, rootCmd.Flags()
opts, flags, _ := setupCommonRootCommand(rootCmd)
return opts, flags
}
// FlagErrorFunc prints an error message which matches the format of the
@ -82,8 +78,12 @@ func FlagErrorFunc(cmd *cobra.Command, err error) error {
return nil
}
usage := ""
if cmd.HasSubCommands() {
usage = "\n\n" + cmd.UsageString()
}
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,
}
}
@ -101,13 +101,7 @@ type TopLevelCommand struct {
// NewTopLevelCommand returns a new TopLevelCommand object
func NewTopLevelCommand(cmd *cobra.Command, dockerCli *command.DockerCli, opts *cliflags.ClientOptions, flags *pflag.FlagSet) *TopLevelCommand {
return &TopLevelCommand{
cmd: cmd,
dockerCli: dockerCli,
opts: opts,
flags: flags,
args: os.Args[1:],
}
return &TopLevelCommand{cmd, dockerCli, opts, flags, os.Args[1:]}
}
// SetArgs sets the args (default os.Args[:1] used to invoke the command
@ -162,8 +156,8 @@ func (tcmd *TopLevelCommand) HandleGlobalFlags() (*cobra.Command, []string, erro
}
// Initialize finalises global option parsing and initializes the docker client.
func (tcmd *TopLevelCommand) Initialize(ops ...command.CLIOption) error {
tcmd.opts.SetDefaultOptions(tcmd.flags)
func (tcmd *TopLevelCommand) Initialize(ops ...command.InitializeOpt) error {
tcmd.opts.Common.SetDefaultOptions(tcmd.flags)
return tcmd.dockerCli.Initialize(tcmd.opts, ops...)
}
@ -186,16 +180,6 @@ func DisableFlagsInUseLine(cmd *cobra.Command) {
})
}
// HasCompletionArg returns true if a cobra completion arg request is found.
func HasCompletionArg(args []string) bool {
for _, arg := range args {
if arg == cobra.ShellCompRequestCmd || arg == cobra.ShellCompNoDescRequestCmd {
return true
}
}
return false
}
var helpCommand = &cobra.Command{
Use: "help [command]",
Short: "Help about the command",
@ -226,13 +210,9 @@ func isExperimental(cmd *cobra.Command) bool {
}
func additionalHelp(cmd *cobra.Command) string {
if msg, ok := cmd.Annotations["additionalHelp"]; ok {
out := cmd.OutOrStderr()
if _, isTerminal := term.GetFdInfo(out); !isTerminal {
return msg
}
if additionalHelp, ok := cmd.Annotations["additionalHelp"]; ok {
style := aec.EmptyBuilder.Bold().ANSI
return style.Apply(msg)
return style.Apply(additionalHelp)
}
return ""
}
@ -242,11 +222,7 @@ func hasAdditionalHelp(cmd *cobra.Command) bool {
}
func isPlugin(cmd *cobra.Command) bool {
return cmd.Annotations[metadata.CommandAnnotationPlugin] == "true"
}
func hasAliases(cmd *cobra.Command) bool {
return len(cmd.Aliases) > 0 || cmd.Annotations["aliases"] != ""
return cmd.Annotations[pluginmanager.CommandAnnotationPlugin] == "true"
}
func hasSubCommands(cmd *cobra.Command) bool {
@ -257,69 +233,16 @@ func hasManagementSubCommands(cmd *cobra.Command) bool {
return len(managementSubCommands(cmd)) > 0
}
func hasSwarmSubCommands(cmd *cobra.Command) bool {
return len(orchestratorSubCommands(cmd)) > 0
}
func hasInvalidPlugins(cmd *cobra.Command) bool {
return len(invalidPlugins(cmd)) > 0
}
func hasTopCommands(cmd *cobra.Command) bool {
return len(topCommands(cmd)) > 0
}
// commandAliases is a templating function to return aliases for the command,
// formatted as the full command as they're called (contrary to the default
// Aliases function, which only returns the subcommand).
func commandAliases(cmd *cobra.Command) string {
if cmd.Annotations["aliases"] != "" {
return cmd.Annotations["aliases"]
}
var parentPath string
if cmd.HasParent() {
parentPath = cmd.Parent().CommandPath() + " "
}
aliases := cmd.CommandPath()
for _, alias := range cmd.Aliases {
aliases += ", " + parentPath + alias
}
return aliases
}
func topCommands(cmd *cobra.Command) []*cobra.Command {
cmds := []*cobra.Command{}
if cmd.Parent() != nil {
// for now, only use top-commands for the root-command, and skip
// for sub-commands
return cmds
}
for _, sub := range cmd.Commands() {
if isPlugin(sub) || !sub.IsAvailableCommand() {
continue
}
if _, ok := sub.Annotations["category-top"]; ok {
cmds = append(cmds, sub)
}
}
sort.SliceStable(cmds, func(i, j int) bool {
return sortorder.NaturalLess(cmds[i].Annotations["category-top"], cmds[j].Annotations["category-top"])
})
return cmds
}
func operationSubCommands(cmd *cobra.Command) []*cobra.Command {
cmds := []*cobra.Command{}
for _, sub := range cmd.Commands() {
if isPlugin(sub) {
continue
}
if _, ok := sub.Annotations["category-top"]; ok {
if cmd.Parent() == nil {
// for now, only use top-commands for the root-command
continue
}
}
if sub.IsAvailableCommand() && !sub.HasSubCommands() {
cmds = append(cmds, sub)
}
@ -327,10 +250,8 @@ func operationSubCommands(cmd *cobra.Command) []*cobra.Command {
return cmds
}
const defaultTermWidth = 80
func wrappedFlagUsages(cmd *cobra.Command) string {
width := defaultTermWidth
width := 80
if ws, err := term.GetWinsize(0); err == nil {
width = int(ws.Width)
}
@ -346,9 +267,9 @@ func decoratedName(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 := ""
if v, ok := cmd.Annotations[metadata.CommandAnnotationPluginVersion]; ok && v != "" {
if v, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVersion]; ok && v != "" {
version = ", " + v
}
return fmt.Sprintf("(%s%s)", vendor, version)
@ -357,27 +278,6 @@ func vendorAndVersion(cmd *cobra.Command) string {
}
func managementSubCommands(cmd *cobra.Command) []*cobra.Command {
cmds := []*cobra.Command{}
for _, sub := range allManagementSubCommands(cmd) {
if _, ok := sub.Annotations["swarm"]; ok {
continue
}
cmds = append(cmds, sub)
}
return cmds
}
func orchestratorSubCommands(cmd *cobra.Command) []*cobra.Command {
cmds := []*cobra.Command{}
for _, sub := range allManagementSubCommands(cmd) {
if _, ok := sub.Annotations["swarm"]; ok {
cmds = append(cmds, sub)
}
}
return cmds
}
func allManagementSubCommands(cmd *cobra.Command) []*cobra.Command {
cmds := []*cobra.Command{}
for _, sub := range cmd.Commands() {
if isPlugin(sub) {
@ -407,10 +307,10 @@ func invalidPlugins(cmd *cobra.Command) []*cobra.Command {
}
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 .HasSubCommands}} {{ .CommandPath}}{{- if .HasAvailableFlags}} [OPTIONS]{{end}} COMMAND{{end}}
@ -426,10 +326,10 @@ EXPERIMENTAL:
https://docs.docker.com/go/experimental/
{{- end}}
{{- if hasAliases . }}
{{- if gt .Aliases 0}}
Aliases:
{{ commandAliases . }}
{{.NameAndAliases}}
{{- end}}
{{- if .HasExample}}
@ -438,36 +338,18 @@ Examples:
{{ .Example }}
{{- end}}
{{- if .HasParent}}
{{- if .HasAvailableFlags}}
Options:
{{ wrappedFlagUsages . | trimRightSpace}}
{{- end}}
{{- end}}
{{- if hasTopCommands .}}
Common Commands:
{{- range topCommands .}}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
{{- end}}
{{- end}}
{{- if hasManagementSubCommands . }}
Management Commands:
{{- range managementSubCommands . }}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
{{- end}}
{{- end}}
{{- if hasSwarmSubCommands . }}
Swarm Commands:
{{- range orchestratorSubCommands . }}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
{{- end}}
{{- end}}
@ -488,14 +370,6 @@ Invalid Plugins:
{{rpad .Name .NamePadding }} {{invalidPluginReason .}}
{{- end}}
{{- end}}
{{- if not .HasParent}}
{{- if .HasAvailableFlags}}
Global Options:
{{ wrappedFlagUsages . | trimRightSpace}}
{{- end}}
{{- end}}
{{- if .HasSubCommands }}
@ -505,9 +379,8 @@ Run '{{.CommandPath}} COMMAND --help' for more information on a command.
{{- if hasAdditionalHelp .}}
{{ additionalHelp . }}
{{- end}}
`
const helpTemplate = `
{{- if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
var helpTemplate = `
{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`

View File

@ -3,7 +3,7 @@ package cli
import (
"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/spf13/cobra"
"gotest.tools/v3/assert"
@ -49,9 +49,9 @@ func TestVendorAndVersion(t *testing.T) {
cmd := &cobra.Command{
Use: "test",
Annotations: map[string]string{
metadata.CommandAnnotationPlugin: "true",
metadata.CommandAnnotationPluginVendor: tc.vendor,
metadata.CommandAnnotationPluginVersion: tc.version,
pluginmanager.CommandAnnotationPlugin: "true",
pluginmanager.CommandAnnotationPluginVendor: tc.vendor,
pluginmanager.CommandAnnotationPluginVersion: tc.version,
},
}
assert.Equal(t, vendorAndVersion(cmd), tc.expected)
@ -69,8 +69,8 @@ func TestInvalidPlugin(t *testing.T) {
assert.Assert(t, is.Len(invalidPlugins(root), 0))
sub1.Annotations = map[string]string{
metadata.CommandAnnotationPlugin: "true",
metadata.CommandAnnotationPluginInvalid: "foo",
pluginmanager.CommandAnnotationPlugin: "true",
pluginmanager.CommandAnnotationPluginInvalid: "foo",
}
root.AddCommand(sub1, sub2)
sub1.AddCommand(sub1sub1, sub1sub2)
@ -78,28 +78,11 @@ func TestInvalidPlugin(t *testing.T) {
assert.DeepEqual(t, invalidPlugins(root), []*cobra.Command{sub1}, cmpopts.IgnoreUnexported(cobra.Command{}))
}
func TestCommandAliases(t *testing.T) {
root := &cobra.Command{Use: "root"}
sub := &cobra.Command{Use: "subcommand", Aliases: []string{"alias1", "alias2"}}
sub2 := &cobra.Command{Use: "subcommand2", Annotations: map[string]string{"aliases": "root foo, root bar"}}
root.AddCommand(sub)
root.AddCommand(sub2)
assert.Equal(t, hasAliases(sub), true)
assert.Equal(t, commandAliases(sub), "root subcommand, root alias1, root alias2")
assert.Equal(t, hasAliases(sub2), true)
assert.Equal(t, commandAliases(sub2), "root foo, root bar")
sub.Annotations = map[string]string{"aliases": "custom alias, custom alias 2"}
assert.Equal(t, hasAliases(sub), true)
assert.Equal(t, commandAliases(sub), "custom alias, custom alias 2")
}
func TestDecoratedName(t *testing.T) {
root := &cobra.Command{Use: "root"}
topLevelCommand := &cobra.Command{Use: "pluginTopLevelCommand"}
root.AddCommand(topLevelCommand)
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*")
}

View File

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

View File

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

View File

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

View File

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

View File

@ -6,8 +6,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/docker/api/types/checkpoint"
"github.com/docker/docker/api/types"
"github.com/spf13/cobra"
)
@ -28,28 +27,31 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
opts.container = args[0]
opts.checkpoint = args[1]
return runCreate(cmd.Context(), dockerCli, opts)
return runCreate(dockerCli, opts)
},
ValidArgsFunction: completion.NoComplete,
}
flags := cmd.Flags()
flags.BoolVar(&opts.leaveRunning, "leave-running", false, "Leave the container running after checkpoint")
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
return cmd
}
func runCreate(ctx context.Context, dockerCLI command.Cli, opts createOptions) error {
err := dockerCLI.Client().CheckpointCreate(ctx, opts.container, checkpoint.CreateOptions{
func runCreate(dockerCli command.Cli, opts createOptions) error {
client := dockerCli.Client()
checkpointOpts := types.CheckpointCreateOptions{
CheckpointID: opts.checkpoint,
CheckpointDir: opts.checkpointDir,
Exit: !opts.leaveRunning,
})
}
err := client.CheckpointCreate(context.Background(), opts.container, checkpointOpts)
if err != nil {
return err
}
_, _ = fmt.Fprintln(dockerCLI.Out(), opts.checkpoint)
fmt.Fprintf(dockerCli.Out(), "%s\n", opts.checkpoint)
return nil
}

View File

@ -1,14 +1,13 @@
package checkpoint
import (
"errors"
"io"
"strconv"
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/checkpoint"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
@ -16,21 +15,21 @@ import (
func TestCheckpointCreateErrors(t *testing.T) {
testCases := []struct {
args []string
checkpointCreateFunc func(container string, options checkpoint.CreateOptions) error
checkpointCreateFunc func(container string, options types.CheckpointCreateOptions) error
expectedError string
}{
{
args: []string{"too-few-arguments"},
expectedError: "requires 2 arguments",
expectedError: "requires exactly 2 arguments",
},
{
args: []string{"too", "many", "arguments"},
expectedError: "requires 2 arguments",
expectedError: "requires exactly 2 arguments",
},
{
args: []string{"foo", "bar"},
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
return errors.New("error creating checkpoint for container foo")
checkpointCreateFunc: func(container string, options types.CheckpointCreateOptions) error {
return errors.Errorf("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.SetArgs(tc.args)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetOut(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestCheckpointCreateWithOptions(t *testing.T) {
const (
containerName = "container-foo"
checkpointName = "checkpoint-bar"
checkpointDir = "/dir/foo"
)
for _, tc := range []bool{true, false} {
leaveRunning := strconv.FormatBool(tc)
t.Run("leave-running="+leaveRunning, func(t *testing.T) {
var actualContainerName string
var actualOptions checkpoint.CreateOptions
cli := test.NewFakeCli(&fakeClient{
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
actualContainerName = container
actualOptions = options
return nil
},
})
cmd := newCreateCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{containerName, checkpointName})
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 := checkpoint.CreateOptions{
CheckpointID: checkpointName,
CheckpointDir: checkpointDir,
Exit: !tc,
}
assert.Check(t, is.Equal(actualOptions, expected))
assert.Check(t, is.Equal(strings.TrimSpace(cli.OutBuffer().String()), checkpointName))
})
}
var containerID, checkpointID, checkpointDir string
var exit bool
cli := test.NewFakeCli(&fakeClient{
checkpointCreateFunc: func(container string, options types.CheckpointCreateOptions) error {
containerID = container
checkpointID = options.CheckpointID
checkpointDir = options.CheckpointDir
exit = options.Exit
return nil
},
})
cmd := newCreateCommand(cli)
checkpoint := "checkpoint-bar"
cmd.SetArgs([]string{"container-foo", checkpoint})
cmd.Flags().Set("leave-running", "true")
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Equal("container-foo", containerID))
assert.Check(t, is.Equal(checkpoint, checkpointID))
assert.Check(t, is.Equal("/dir/foo", checkpointDir))
assert.Check(t, is.Equal(false, exit))
assert.Check(t, is.Equal(checkpoint, strings.TrimSpace(cli.OutBuffer().String())))
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,114 +1,96 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.22
package command
import (
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strconv"
"sync"
"strings"
"time"
"github.com/docker/cli/cli/config"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
dcontext "github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/store"
"github.com/docker/cli/cli/debug"
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/trust"
"github.com/docker/cli/cli/version"
dopts "github.com/docker/cli/opts"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/client"
"github.com/docker/go-connections/tlsconfig"
"github.com/moby/term"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/theupdateframework/notary"
notaryclient "github.com/theupdateframework/notary/client"
"github.com/theupdateframework/notary/passphrase"
)
const defaultInitTimeout = 2 * time.Second
// Streams is an interface which exposes the standard input and output streams
type Streams interface {
In() *streams.In
Out() *streams.Out
Err() *streams.Out
Err() io.Writer
}
// Cli represents the docker command line client.
type Cli interface {
Client() client.APIClient
Streams
Out() *streams.Out
Err() io.Writer
In() *streams.In
SetIn(in *streams.In)
Apply(ops ...CLIOption) error
config.Provider
Apply(ops ...DockerCliOption) error
ConfigFile() *configfile.ConfigFile
ServerInfo() ServerInfo
ClientInfo() ClientInfo
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
DefaultVersion() string
CurrentVersion() string
ManifestStore() manifeststore.Store
RegistryClient(bool) registryclient.RegistryClient
ContentTrustEnabled() bool
BuildKitEnabled() (bool, error)
ContextStore() store.Store
CurrentContext() string
StackOrchestrator(flagValue string) (Orchestrator, error)
DockerEndpoint() docker.Endpoint
TelemetryClient
DeprecatedNotaryClient
DeprecatedManifestClient
}
// DockerCli is an instance the docker command line client.
// Instances of the client can be returned from NewDockerCli.
type DockerCli struct {
configFile *configfile.ConfigFile
options *cliflags.ClientOptions
in *streams.In
out *streams.Out
err *streams.Out
err io.Writer
client client.APIClient
serverInfo ServerInfo
clientInfo *ClientInfo
contentTrust bool
contextStore store.Store
currentContext string
init sync.Once
initErr error
dockerEndpoint docker.Endpoint
contextStoreConfig store.Config
initTimeout time.Duration
res telemetryResource
// baseCtx is the base context used for internal operations. In the future
// this may be replaced by explicitly passing a context to functions that
// need it.
baseCtx context.Context
enableGlobalMeter, enableGlobalTracer bool
}
// DefaultVersion returns [api.DefaultVersion].
func (*DockerCli) DefaultVersion() string {
return api.DefaultVersion
}
// CurrentVersion returns the API version currently negotiated, or the default
// version otherwise.
func (cli *DockerCli) CurrentVersion() string {
_ = cli.initialize()
if cli.client == nil {
return api.DefaultVersion
}
return cli.client.ClientVersion()
// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
func (cli *DockerCli) DefaultVersion() string {
return cli.ClientInfo().DefaultVersion
}
// Client returns the 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
}
@ -118,7 +100,7 @@ func (cli *DockerCli) Out() *streams.Out {
}
// Err returns the writer used for stderr
func (cli *DockerCli) Err() *streams.Out {
func (cli *DockerCli) Err() io.Writer {
return cli.err
}
@ -143,87 +125,86 @@ func ShowHelp(err io.Writer) func(*cobra.Command, []string) error {
// ConfigFile returns the ConfigFile
func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
// TODO(thaJeztah): when would this happen? Is this only in tests (where cli.Initialize() is not called first?)
if cli.configFile == nil {
cli.configFile = config.LoadDefaultConfigFile(cli.err)
cli.loadConfigFile()
}
return cli.configFile
}
func (cli *DockerCli) loadConfigFile() {
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
}
// ServerInfo returns the server version details for the host this client is
// connected to
func (cli *DockerCli) ServerInfo() ServerInfo {
_ = cli.initialize()
return cli.serverInfo
}
// ClientInfo returns the client details for the cli
func (cli *DockerCli) ClientInfo() ClientInfo {
if cli.clientInfo == nil {
if err := cli.loadClientInfo(); err != nil {
panic(err)
}
}
return *cli.clientInfo
}
func (cli *DockerCli) loadClientInfo() error {
var v string
if cli.client != nil {
v = cli.client.ClientVersion()
} else {
v = api.DefaultVersion
}
cli.clientInfo = &ClientInfo{
DefaultVersion: v,
HasExperimental: true,
}
return nil
}
// ContentTrustEnabled returns whether content trust has been enabled by an
// environment variable.
func (cli *DockerCli) ContentTrustEnabled() bool {
return cli.contentTrust
}
// BuildKitEnabled returns buildkit is enabled or not.
func (cli *DockerCli) BuildKitEnabled() (bool, error) {
// use DOCKER_BUILDKIT env var value if set and not empty
if v := os.Getenv("DOCKER_BUILDKIT"); v != "" {
enabled, err := strconv.ParseBool(v)
// BuildKitEnabled returns whether buildkit is enabled either through a daemon setting
// or otherwise the client-side DOCKER_BUILDKIT environment variable
func BuildKitEnabled(si ServerInfo) (bool, error) {
buildkitEnabled := si.BuildkitVersion == types.BuilderBuildKit
if buildkitEnv := os.Getenv("DOCKER_BUILDKIT"); buildkitEnv != "" {
var err error
buildkitEnabled, err = strconv.ParseBool(buildkitEnv)
if err != nil {
return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
}
return enabled, nil
}
// if a builder alias is defined, we are using BuildKit
aliasMap := cli.ConfigFile().Aliases
if _, ok := aliasMap["builder"]; ok {
return true, nil
}
si := cli.ServerInfo()
if si.BuildkitVersion == types.BuilderBuildKit {
// The daemon advertised BuildKit as the preferred builder; this may
// be either a Linux daemon or a Windows daemon with experimental
// BuildKit support enabled.
return true, nil
}
// otherwise, assume BuildKit is enabled for Linux, but disabled for
// Windows / WCOW, which does not yet support BuildKit by default.
return si.OSType != "windows", nil
return buildkitEnabled, nil
}
// HooksEnabled returns whether plugin hooks are enabled.
func (cli *DockerCli) HooksEnabled() bool {
// use DOCKER_CLI_HOOKS env var value if set and not empty
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
}
return enabled
}
// legacy support DOCKER_CLI_HINTS env var
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
}
return enabled
}
featuresMap := cli.ConfigFile().Features
if v, ok := featuresMap["hooks"]; ok {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
}
return enabled
}
// default to false
return false
// ManifestStore returns a store for local manifests
func (cli *DockerCli) ManifestStore() manifeststore.Store {
// TODO: support override default location from config file
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)
}
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
}
// InitializeOpt is the type of the functional options passed to DockerCli.Initialize
type InitializeOpt func(dockerCli *DockerCli) error
// WithInitializeClient is passed to DockerCli.Initialize by callers who wish to set a particular API Client for use by the CLI.
func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) CLIOption {
func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) InitializeOpt {
return func(dockerCli *DockerCli) error {
var err error
dockerCli.client, err = makeClient(dockerCli)
@ -233,61 +214,79 @@ func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClien
// Initialize the dockerCli runs initialization that must happen after command
// line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error {
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...InitializeOpt) error {
var err error
for _, o := range ops {
if err := o(cli); err != nil {
return err
}
}
cliflags.SetLogLevel(opts.LogLevel)
cliflags.SetLogLevel(opts.Common.LogLevel)
if opts.ConfigDir != "" {
config.SetDir(opts.ConfigDir)
cliconfig.SetDir(opts.ConfigDir)
}
if opts.Debug {
if opts.Common.Debug {
debug.Enable()
}
if opts.Context != "" && len(opts.Hosts) > 0 {
return errors.New("conflicting options: cannot specify both --host and --context")
}
cli.options = opts
cli.configFile = config.LoadDefaultConfigFile(cli.err)
cli.currentContext = resolveContextName(cli.options, cli.configFile)
cli.loadConfigFile()
baseContextStore := store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig)
cli.contextStore = &ContextStoreWithDefault{
Store: store.New(config.ContextStoreDir(), cli.contextStoreConfig),
Store: baseContextStore,
Resolver: func() (*DefaultContext, error) {
return ResolveDefaultContext(cli.options, cli.contextStoreConfig)
return ResolveDefaultContext(opts.Common, cli.ConfigFile(), cli.contextStoreConfig, cli.Err())
},
}
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")
}
// TODO(krissetto): pass ctx to the funcs instead of using this
if cli.enableGlobalMeter {
cli.createGlobalMeterProvider(cli.baseCtx)
if cli.client == nil {
cli.client, err = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile)
if tlsconfig.IsErrEncryptedKey(err) {
passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
newClient := func(password string) (client.APIClient, error) {
cli.dockerEndpoint.TLSPassword = password
return newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile)
}
cli.client, err = getClientWithPassword(passRetriever, newClient)
}
if err != nil {
return err
}
}
if cli.enableGlobalTracer {
cli.createGlobalTracerProvider(cli.baseCtx)
cli.initializeFromClient()
if err := cli.loadClientInfo(); err != nil {
return err
}
filterResourceAttributesEnvvar()
return nil
}
// NewAPIClientFromFlags creates a new APIClient from command line flags
func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
if opts.Context != "" && len(opts.Hosts) > 0 {
return nil, errors.New("conflicting options: cannot specify both --host and --context")
}
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
storeConfig := DefaultContextStoreConfig()
contextStore := &ContextStoreWithDefault{
Store: store.New(config.ContextStoreDir(), storeConfig),
store := &ContextStoreWithDefault{
Store: store.New(cliconfig.ContextStoreDir(), storeConfig),
Resolver: func() (*DefaultContext, error) {
return ResolveDefaultContext(opts, storeConfig)
return ResolveDefaultContext(opts, configFile, storeConfig, ioutil.Discard)
},
}
endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile))
contextName, err := resolveContextName(opts, configFile, store)
if err != nil {
return nil, err
}
endpoint, err := resolveDockerEndpoint(store, contextName)
if err != nil {
return nil, errors.Wrap(err, "unable to resolve docker endpoint")
}
@ -295,21 +294,20 @@ func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.
}
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
opts, err := ep.ClientOpts()
clientOpts, err := ep.ClientOpts()
if err != nil {
return nil, err
}
if len(configFile.HTTPHeaders) > 0 {
opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders))
customHeaders := make(map[string]string, len(configFile.HTTPHeaders))
for k, v := range configFile.HTTPHeaders {
customHeaders[k] = v
}
opts = append(opts, withCustomHeadersFromEnv(), client.WithUserAgent(UserAgent()))
return client.NewClientWithOpts(opts...)
customHeaders["User-Agent"] = UserAgent()
clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))
return client.NewClientWithOpts(clientOpts...)
}
func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) {
if s == nil {
return docker.Endpoint{}, errors.New("no context store initialized")
}
ctxMeta, err := s.GetMetadata(contextName)
if err != nil {
return docker.Endpoint{}, err
@ -322,11 +320,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)
func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) {
// defaultToTLS determines whether we should use a TLS host as default
// if nothing was configured by the user.
defaultToTLS := opts.TLSOptions != nil
host, err := getServerHost(opts.Hosts, defaultToTLS)
func resolveDefaultDockerEndpoint(opts *cliflags.CommonOptions) (docker.Endpoint, error) {
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
if err != nil {
return docker.Endpoint{}, err
}
@ -353,16 +348,15 @@ func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint
}, nil
}
func (cli *DockerCli) getInitTimeout() time.Duration {
if cli.initTimeout != 0 {
return cli.initTimeout
}
return defaultInitTimeout
}
func (cli *DockerCli) initializeFromClient() {
ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout())
defer cancel()
ctx := context.Background()
if strings.HasPrefix(cli.DockerEndpoint().Host, "tcp://") {
// @FIXME context.WithTimeout doesn't work with connhelper / ssh connections
// time="2020-04-10T10:16:26Z" level=warning msg="commandConn.CloseWrite: commandconn: failed to wait: signal: killed"
var cancel func()
ctx, cancel = context.WithTimeout(ctx, 2*time.Second)
defer cancel()
}
ping, err := cli.client.Ping(ctx)
if err != nil {
@ -379,113 +373,65 @@ func (cli *DockerCli) initializeFromClient() {
HasExperimental: ping.Experimental,
OSType: ping.OSType,
BuildkitVersion: ping.BuilderVersion,
SwarmStatus: ping.SwarmStatus,
}
cli.client.NegotiateAPIVersionPing(ping)
}
func getClientWithPassword(passRetriever notary.PassRetriever, newClient func(password string) (client.APIClient, error)) (client.APIClient, error) {
for attempts := 0; ; attempts++ {
passwd, giveup, err := passRetriever("private", "encrypted TLS private", false, attempts)
if giveup || err != nil {
return nil, errors.Wrap(err, "private key is encrypted, but could not get passphrase")
}
apiclient, err := newClient(passwd)
if !tlsconfig.IsErrEncryptedKey(err) {
return apiclient, err
}
}
}
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
}
// ContextStore returns the ContextStore
func (cli *DockerCli) ContextStore() store.Store {
return cli.contextStore
}
// CurrentContext returns the current context name, based on flags,
// environment variables and the cli configuration file, in the following
// order of preference:
//
// 1. The "--context" command-line option.
// 2. The "DOCKER_CONTEXT" environment variable ([EnvOverrideContext]).
// 3. The current context as configured through the in "currentContext"
// field in the CLI configuration file ("~/.docker/config.json").
// 4. If no context is configured, use the "default" context.
//
// # Fallbacks for backward-compatibility
//
// To preserve backward-compatibility with the "pre-contexts" behavior,
// the "default" context is used if:
//
// - The "--host" option is set
// - The "DOCKER_HOST" ([client.EnvOverrideHost]) environment variable is set
// to a non-empty value.
//
// In these cases, the default context is used, which uses the host as
// specified in "DOCKER_HOST", and TLS config from flags/env vars.
//
// Setting both the "--context" and "--host" flags is ambiguous and results
// in an error when the cli is started.
//
// CurrentContext does not validate if the given context exists or if it's
// valid; errors may occur when trying to use it.
// CurrentContext returns the current context name
func (cli *DockerCli) CurrentContext() string {
return cli.currentContext
}
// CurrentContext returns the current context name, based on flags,
// environment variables and the cli configuration file. It does not
// validate if the given context exists or if it's valid; errors may
// occur when trying to use it.
//
// Refer to [DockerCli.CurrentContext] above for further details.
func resolveContextName(opts *cliflags.ClientOptions, cfg *configfile.ConfigFile) string {
if opts != nil && opts.Context != "" {
return opts.Context
// StackOrchestrator resolves which stack orchestrator is in use
func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error) {
currentContext := cli.CurrentContext()
ctxRaw, err := cli.ContextStore().GetMetadata(currentContext)
if store.IsErrContextDoesNotExist(err) {
// case where the currentContext has been removed (CLI behavior is to fallback to using DOCKER_HOST based resolution)
return GetStackOrchestrator(flagValue, "", cli.ConfigFile().StackOrchestrator, cli.Err())
}
if opts != nil && len(opts.Hosts) > 0 {
return DefaultContextName
if err != nil {
return "", err
}
if os.Getenv(client.EnvOverrideHost) != "" {
return DefaultContextName
ctxMeta, err := GetDockerContext(ctxRaw)
if err != nil {
return "", err
}
if ctxName := os.Getenv(EnvOverrideContext); ctxName != "" {
return ctxName
}
if cfg != nil && cfg.CurrentContext != "" {
// We don't validate if this context exists: errors may occur when trying to use it.
return cfg.CurrentContext
}
return DefaultContextName
ctxOrchestrator := string(ctxMeta.StackOrchestrator)
return GetStackOrchestrator(flagValue, ctxOrchestrator, cli.ConfigFile().StackOrchestrator, cli.Err())
}
// DockerEndpoint returns the current docker endpoint
func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
if err := cli.initialize(); err != nil {
// Note that we're not terminating here, as this function may be used
// in cases where we're able to continue.
_, _ = fmt.Fprintln(cli.Err(), cli.initErr)
}
return cli.dockerEndpoint
}
func (cli *DockerCli) getDockerEndPoint() (ep docker.Endpoint, err error) {
cn := cli.CurrentContext()
if cn == DefaultContextName {
return resolveDefaultDockerEndpoint(cli.options)
}
return resolveDockerEndpoint(cli.contextStore, cn)
}
func (cli *DockerCli) initialize() error {
cli.init.Do(func() {
cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint()
if cli.initErr != nil {
cli.initErr = errors.Wrap(cli.initErr, "unable to resolve docker endpoint")
return
}
if cli.client == nil {
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil {
return
}
}
if cli.baseCtx == nil {
cli.baseCtx = context.Background()
}
cli.initializeFromClient()
})
return cli.initErr
}
// Apply all the operation on the cli
func (cli *DockerCli) Apply(ops ...CLIOption) error {
func (cli *DockerCli) Apply(ops ...DockerCliOption) error {
for _, op := range ops {
if err := op(cli); err != nil {
return err
@ -500,44 +446,56 @@ type ServerInfo struct {
HasExperimental bool
OSType string
BuildkitVersion types.BuilderVersion
}
// SwarmStatus provides information about the current swarm status of the
// engine, obtained from the "Swarm" header in the API response.
//
// It can be a nil struct if the API version does not provide this header
// in the ping response, or if an error occurred, in which case the client
// should use other ways to get the current swarm status, such as the /swarm
// endpoint.
SwarmStatus *swarm.Status
// ClientInfo stores details about the supported features of the client
type ClientInfo struct {
// Deprecated: experimental CLI features always enabled. This field is kept
// for backward-compatibility, and is always "true".
HasExperimental bool
DefaultVersion string
}
// NewDockerCli returns a DockerCli instance with all operators applied on it.
// It applies by default the standard streams, and the content trust from
// environment.
func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
defaultOps := []CLIOption{
func NewDockerCli(ops ...DockerCliOption) (*DockerCli, error) {
cli := &DockerCli{}
defaultOps := []DockerCliOption{
WithContentTrustFromEnv(),
WithDefaultContextStoreConfig(),
WithStandardStreams(),
}
cli.contextStoreConfig = DefaultContextStoreConfig()
ops = append(defaultOps, ops...)
cli := &DockerCli{baseCtx: context.Background()}
if err := cli.Apply(ops...); err != nil {
return nil, err
}
if cli.out == nil || cli.in == nil || cli.err == nil {
stdin, stdout, stderr := term.StdStreams()
if cli.in == nil {
cli.in = streams.NewIn(stdin)
}
if cli.out == nil {
cli.out = streams.NewOut(stdout)
}
if cli.err == nil {
cli.err = stderr
}
}
return cli, nil
}
func getServerHost(hosts []string, defaultToTLS bool) (string, error) {
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
var host string
switch len(hosts) {
case 0:
return dopts.ParseHost(defaultToTLS, os.Getenv(client.EnvOverrideHost))
host = os.Getenv("DOCKER_HOST")
case 1:
return dopts.ParseHost(defaultToTLS, hosts[0])
host = hosts[0]
default:
return "", errors.New("Specify only one -H")
return "", errors.New("Please specify only one -H")
}
return dopts.ParseHost(tlsOptions != nil, host)
}
// UserAgent returns the user agent string used for making API requests
@ -545,8 +503,42 @@ func UserAgent() string {
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
}
// resolveContextName resolves the current context name with the following rules:
// - setting both --context and --host flags is ambiguous
// - if --context is set, use this value
// - if --host flag or DOCKER_HOST is set, fallbacks to use the same logic as before context-store was added
// for backward compatibility with existing scripts
// - if DOCKER_CONTEXT is set, use this value
// - if Config file has a globally set "CurrentContext", use this value
// - fallbacks to default HOST, uses TLS config from flags/env vars
func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigFile, contextstore store.Reader) (string, error) {
if opts.Context != "" && len(opts.Hosts) > 0 {
return "", errors.New("Conflicting options: either specify --host or --context, not both")
}
if opts.Context != "" {
return opts.Context, nil
}
if len(opts.Hosts) > 0 {
return DefaultContextName, nil
}
if _, present := os.LookupEnv("DOCKER_HOST"); present {
return DefaultContextName, nil
}
if ctxName, ok := os.LookupEnv("DOCKER_CONTEXT"); ok {
return ctxName, nil
}
if config != nil && config.CurrentContext != "" {
_, err := contextstore.GetMetadata(config.CurrentContext)
if store.IsErrContextDoesNotExist(err) {
return "", errors.Errorf("Current context %q is not found on the file system, please check your config file at %s", config.CurrentContext, config.Filename)
}
return config.CurrentContext, err
}
return DefaultContextName, nil
}
var defaultStoreEndpoints = []store.NamedTypeGetter{
store.EndpointTypeGetter(docker.DockerEndpoint, func() any { return &docker.EndpointMeta{} }),
store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }),
}
// RegisterDefaultStoreEndpoints registers a new named endpoint
@ -560,7 +552,7 @@ func RegisterDefaultStoreEndpoints(ep ...store.NamedTypeGetter) {
// DefaultContextStoreConfig returns a new store.Config with the default set of endpoints configured.
func DefaultContextStoreConfig() store.Config {
return store.NewConfig(
func() any { return &DockerContext{} },
func() interface{} { return &DockerContext{} },
defaultStoreEndpoints...,
)
}

View File

@ -1,56 +0,0 @@
package command
import (
"context"
"path/filepath"
"github.com/docker/cli/cli/config"
manifeststore "github.com/docker/cli/cli/manifest/store"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/cli/trust"
"github.com/docker/docker/api/types/registry"
notaryclient "github.com/theupdateframework/notary/client"
)
type DeprecatedNotaryClient interface {
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
//
// Deprecated: use [trust.GetNotaryRepository] instead. This method is no longer used and will be removed in the next release.
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
}
type DeprecatedManifestClient interface {
// ManifestStore returns a store for local manifests
//
// Deprecated: use [manifeststore.NewStore] instead. This method is no longer used and will be removed in the next release.
ManifestStore() manifeststore.Store
// RegistryClient returns a client for communicating with a Docker distribution
// registry.
//
// Deprecated: use [registryclient.NewRegistryClient]. This method is no longer used and will be removed in the next release.
RegistryClient(bool) registryclient.RegistryClient
}
// 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...)
}
// ManifestStore returns a store for local manifests
//
// Deprecated: use [manifeststore.NewStore] instead. This method is no longer used and will be removed in the next release.
func (*DockerCli) ManifestStore() manifeststore.Store {
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
}
// RegistryClient returns a client for communicating with a Docker distribution
// registry
//
// Deprecated: use [registryclient.NewRegistryClient]. This method is no longer used and will be removed in the next release.
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig {
return ResolveAuthConfig(cli.ConfigFile(), index)
}
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
}

View File

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

View File

@ -8,7 +8,6 @@ import (
)
func contentTrustEnabled(t *testing.T) bool {
t.Helper()
var cli DockerCli
assert.NilError(t, WithContentTrustFromEnv()(&cli))
return cli.contentTrust
@ -16,13 +15,23 @@ func contentTrustEnabled(t *testing.T) bool {
// NB: Do not t.Parallel() this test -- it messes with the process environment.
func TestWithContentTrustFromEnv(t *testing.T) {
const envvar = "DOCKER_CONTENT_TRUST"
t.Setenv(envvar, "true")
assert.Check(t, contentTrustEnabled(t))
t.Setenv(envvar, "false")
assert.Check(t, !contentTrustEnabled(t))
t.Setenv(envvar, "invalid")
assert.Check(t, contentTrustEnabled(t))
envvar := "DOCKER_CONTENT_TRUST"
if orig, ok := os.LookupEnv(envvar); ok {
defer func() {
os.Setenv(envvar, orig)
}()
} else {
defer func() {
os.Unsetenv(envvar)
}()
}
os.Setenv(envvar, "true")
assert.Assert(t, contentTrustEnabled(t))
os.Setenv(envvar, "false")
assert.Assert(t, !contentTrustEnabled(t))
os.Setenv(envvar, "invalid")
assert.Assert(t, contentTrustEnabled(t))
os.Unsetenv(envvar)
assert.Check(t, !contentTrustEnabled(t))
assert.Assert(t, !contentTrustEnabled(t))
}

View File

@ -3,27 +3,24 @@ package command
import (
"bytes"
"context"
"errors"
"crypto/x509"
"fmt"
"io"
"net"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/docker/cli/cli/config"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/flags"
"github.com/docker/cli/cli/streams"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/env"
"gotest.tools/v3/fs"
)
@ -32,107 +29,73 @@ func TestNewAPIClientFromFlags(t *testing.T) {
if runtime.GOOS == "windows" {
host = "npipe://./"
}
opts := &flags.ClientOptions{Hosts: []string{host}}
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host)
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
}
func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
host := ":2375"
slug := "tcp://localhost"
if runtime.GOOS == "windows" {
slug = "tcp://127.0.0.1"
}
opts := &flags.ClientOptions{Hosts: []string{host}}
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), slug+host)
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
}
func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
var received map[string]string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
received = map[string]string{
"My-Header": r.Header.Get("My-Header"),
"User-Agent": r.Header.Get("User-Agent"),
}
_, _ = w.Write([]byte("OK"))
}))
defer ts.Close()
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
opts := &flags.ClientOptions{Hosts: []string{host}}
opts := &flags.CommonOptions{Hosts: []string{host}}
configFile := &configfile.ConfigFile{
HTTPHeaders: map[string]string{
"My-Header": "Custom-Value",
},
}
apiClient, err := NewAPIClientFromFlags(opts, configFile)
apiclient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host)
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
// verify User-Agent is not appended to the configfile. see https://github.com/docker/cli/pull/2756
assert.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"})
assert.Check(t, is.Equal(host, apiclient.DaemonHost()))
expectedHeaders := map[string]string{
"My-Header": "Custom-Value",
"User-Agent": UserAgent(),
}
_, err = apiClient.Ping(context.Background())
assert.NilError(t, err)
assert.DeepEqual(t, received, expectedHeaders)
assert.Check(t, is.DeepEqual(expectedHeaders, apiclient.(*client.Client).CustomHTTPHeaders()))
assert.Check(t, is.Equal(api.DefaultVersion, apiclient.ClientVersion()))
assert.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"})
}
func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
var received http.Header
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
received = r.Header.Clone()
_, _ = w.Write([]byte("OK"))
}))
defer ts.Close()
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
opts := &flags.ClientOptions{Hosts: []string{host}}
func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
host := ":2375"
opts := &flags.CommonOptions{Hosts: []string{host}}
configFile := &configfile.ConfigFile{
HTTPHeaders: map[string]string{
"My-Header": "Custom-Value from config-file",
"My-Header": "Custom-Value",
},
}
// envOverrideHTTPHeaders should override the HTTPHeaders from the config-file,
// so "My-Header" should not be present.
t.Setenv(envOverrideHTTPHeaders, `one=one-value,"two=two,value",three=,four=four-value,four=four-value-override`)
apiClient, err := NewAPIClientFromFlags(opts, configFile)
apiclient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host)
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
assert.Check(t, is.Equal("tcp://localhost"+host, apiclient.DaemonHost()))
expectedHeaders := http.Header{
"One": []string{"one-value"},
"Two": []string{"two,value"},
"Three": []string{""},
"Four": []string{"four-value-override"},
"User-Agent": []string{UserAgent()},
expectedHeaders := map[string]string{
"My-Header": "Custom-Value",
"User-Agent": UserAgent(),
}
_, err = apiClient.Ping(context.Background())
assert.NilError(t, err)
assert.DeepEqual(t, received, expectedHeaders)
assert.Check(t, is.DeepEqual(expectedHeaders, apiclient.(*client.Client).CustomHTTPHeaders()))
assert.Check(t, is.Equal(api.DefaultVersion, apiclient.ClientVersion()))
}
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
const customVersion = "v3.3.3"
const expectedVersion = "3.3.3"
t.Setenv("DOCKER_API_VERSION", customVersion)
t.Setenv("DOCKER_HOST", ":2375")
customVersion := "v3.3.3"
defer env.Patch(t, "DOCKER_API_VERSION", customVersion)()
defer env.Patch(t, "DOCKER_HOST", ":2375")()
opts := &flags.ClientOptions{}
opts := &flags.CommonOptions{}
configFile := &configfile.ConfigFile{}
apiclient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err)
assert.Equal(t, apiclient.ClientVersion(), expectedVersion)
assert.Check(t, is.Equal(customVersion, apiclient.ClientVersion()))
}
func TestNewAPIClientFromFlagsWithHttpProxyEnv(t *testing.T) {
defer env.Patch(t, "HTTP_PROXY", "http://proxy.acme.com:1234")()
defer env.Patch(t, "DOCKER_HOST", "tcp://docker.acme.com:2376")()
opts := &flags.CommonOptions{}
configFile := &configfile.ConfigFile{}
apiclient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err)
transport, ok := apiclient.HTTPClient().Transport.(*http.Transport)
assert.Assert(t, ok)
assert.Assert(t, transport.Proxy != nil)
request, err := http.NewRequest(http.MethodGet, "tcp://docker.acme.com:2376", nil)
assert.NilError(t, err)
url, err := transport.Proxy(request)
assert.NilError(t, err)
assert.Check(t, is.Equal("http://proxy.acme.com:1234", url.String()))
}
type fakeClient struct {
@ -155,9 +118,9 @@ func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
}
func TestInitializeFromClient(t *testing.T) {
const defaultVersion = "v1.55"
defaultVersion := "v1.55"
testcases := []struct {
var testcases = []struct {
doc string
pingFunc func() (types.Ping, error)
expectedServer ServerInfo
@ -188,80 +151,28 @@ func TestInitializeFromClient(t *testing.T) {
},
}
for _, tc := range testcases {
t.Run(tc.doc, func(t *testing.T) {
for _, testcase := range testcases {
testcase := testcase
t.Run(testcase.doc, func(t *testing.T) {
apiclient := &fakeClient{
pingFunc: tc.pingFunc,
pingFunc: testcase.pingFunc,
version: defaultVersion,
}
cli := &DockerCli{client: apiclient}
err := cli.Initialize(flags.NewClientOptions())
assert.NilError(t, err)
assert.DeepEqual(t, cli.ServerInfo(), tc.expectedServer)
assert.Equal(t, apiclient.negotiated, tc.negotiated)
cli.initializeFromClient()
assert.Check(t, is.DeepEqual(testcase.expectedServer, cli.serverInfo))
assert.Check(t, is.Equal(testcase.negotiated, apiclient.negotiated))
})
}
}
// Makes sure we don't hang forever on the initial connection.
// https://github.com/docker/cli/issues/3652
func TestInitializeFromClientHangs(t *testing.T) {
dir := t.TempDir()
socket := filepath.Join(dir, "my.sock")
l, err := net.Listen("unix", socket)
assert.NilError(t, err)
receiveReqCh := make(chan bool)
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Simulate a server that hangs on connections.
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case <-timeoutCtx.Done():
case receiveReqCh <- true: // Blocks until someone receives on the channel.
}
_, _ = w.Write([]byte("OK"))
}))
ts.Listener = l
ts.Start()
defer ts.Close()
opts := &flags.ClientOptions{Hosts: []string{"unix://" + socket}}
configFile := &configfile.ConfigFile{}
apiClient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err)
initializedCh := make(chan bool)
go func() {
cli := &DockerCli{client: apiClient, initTimeout: time.Millisecond}
err := cli.Initialize(flags.NewClientOptions())
assert.Check(t, err)
cli.CurrentVersion()
close(initializedCh)
}()
select {
case <-timeoutCtx.Done():
t.Fatal("timeout waiting for initialization to complete")
case <-initializedCh:
}
select {
case <-timeoutCtx.Done():
t.Fatal("server never received an init request")
case <-receiveReqCh:
}
}
// The CLI no longer disables/hides experimental CLI features, however, we need
// to verify that existing configuration files do not break
func TestExperimentalCLI(t *testing.T) {
defaultVersion := "v1.55"
testcases := []struct {
var testcases = []struct {
doc string
configfile string
}{
@ -277,9 +188,10 @@ func TestExperimentalCLI(t *testing.T) {
},
}
for _, tc := range testcases {
t.Run(tc.doc, func(t *testing.T) {
dir := fs.NewDir(t, tc.doc, fs.WithFile("config.json", tc.configfile))
for _, testcase := range testcases {
testcase := testcase
t.Run(testcase.doc, func(t *testing.T) {
dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile))
defer dir.Remove()
apiclient := &fakeClient{
version: defaultVersion,
@ -288,10 +200,79 @@ func TestExperimentalCLI(t *testing.T) {
},
}
cli := &DockerCli{client: apiclient, err: streams.NewOut(os.Stderr)}
config.SetDir(dir.Path())
cli := &DockerCli{client: apiclient, err: os.Stderr}
cliconfig.SetDir(dir.Path())
err := cli.Initialize(flags.NewClientOptions())
assert.NilError(t, err)
// For backward-compatibility, HasExperimental will always be "true"
assert.Check(t, is.Equal(true, 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 {
testcase := testcase
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)
})
}
}
@ -313,23 +294,23 @@ func TestNewDockerCliAndOperators(t *testing.T) {
outbuf := bytes.NewBuffer(nil)
errbuf := bytes.NewBuffer(nil)
err = cli.Apply(
WithInputStream(io.NopCloser(inbuf)),
WithInputStream(ioutil.NopCloser(inbuf)),
WithOutputStream(outbuf),
WithErrorStream(errbuf),
)
assert.NilError(t, err)
// Check input stream
inputStream, err := io.ReadAll(cli.In())
inputStream, err := ioutil.ReadAll(cli.In())
assert.NilError(t, err)
assert.Equal(t, string(inputStream), "input")
// Check output stream
fmt.Fprintf(cli.Out(), "output")
outputStream, err := io.ReadAll(outbuf)
outputStream, err := ioutil.ReadAll(outbuf)
assert.NilError(t, err)
assert.Equal(t, string(outputStream), "output")
// Check error stream
fmt.Fprintf(cli.Err(), "error")
errStream, err := io.ReadAll(errbuf)
errStream, err := ioutil.ReadAll(errbuf)
assert.NilError(t, err)
assert.Equal(t, string(errStream), "error")
}
@ -342,56 +323,3 @@ func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
})))
assert.Check(t, cli.ContextStore() != nil)
}
func TestHooksEnabled(t *testing.T) {
t.Run("disabled by default", func(t *testing.T) {
cli, err := NewDockerCli()
assert.NilError(t, err)
assert.Check(t, !cli.HooksEnabled())
})
t.Run("enabled in configFile", func(t *testing.T) {
configFile := `{
"features": {
"hooks": "true"
}}`
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
defer dir.Remove()
cli, err := NewDockerCli()
assert.NilError(t, err)
config.SetDir(dir.Path())
assert.Check(t, cli.HooksEnabled())
})
t.Run("env var overrides configFile", func(t *testing.T) {
configFile := `{
"features": {
"hooks": "true"
}}`
t.Setenv("DOCKER_CLI_HOOKS", "false")
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
defer dir.Remove()
cli, err := NewDockerCli()
assert.NilError(t, err)
config.SetDir(dir.Path())
assert.Check(t, !cli.HooksEnabled())
})
t.Run("legacy env var overrides configFile", func(t *testing.T) {
configFile := `{
"features": {
"hooks": "true"
}}`
t.Setenv("DOCKER_CLI_HINTS", "false")
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
defer dir.Remove()
cli, err := NewDockerCli()
assert.NilError(t, err)
config.SetDir(dir.Path())
assert.Check(t, !cli.HooksEnabled())
})
}

View File

@ -28,52 +28,81 @@ import (
// AddCommands adds all the commands from cli/command to the root command
func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
cmd.AddCommand(
// commonly used shorthands
// checkpoint
checkpoint.NewCheckpointCommand(dockerCli),
// config
config.NewConfigCommand(dockerCli),
// container
container.NewContainerCommand(dockerCli),
container.NewRunCommand(dockerCli),
container.NewExecCommand(dockerCli),
container.NewPsCommand(dockerCli),
// image
image.NewImageCommand(dockerCli),
image.NewBuildCommand(dockerCli),
image.NewPullCommand(dockerCli),
image.NewPushCommand(dockerCli),
image.NewImagesCommand(dockerCli),
// builder
builder.NewBuilderCommand(dockerCli),
// manifest
manifest.NewManifestCommand(dockerCli),
// network
network.NewNetworkCommand(dockerCli),
// node
node.NewNodeCommand(dockerCli),
// plugin
plugin.NewPluginCommand(dockerCli),
// registry
registry.NewLoginCommand(dockerCli),
registry.NewLogoutCommand(dockerCli),
registry.NewSearchCommand(dockerCli),
system.NewVersionCommand(dockerCli),
system.NewInfoCommand(dockerCli),
// management commands
builder.NewBuilderCommand(dockerCli),
checkpoint.NewCheckpointCommand(dockerCli),
container.NewContainerCommand(dockerCli),
context.NewContextCommand(dockerCli),
image.NewImageCommand(dockerCli),
manifest.NewManifestCommand(dockerCli),
network.NewNetworkCommand(dockerCli),
plugin.NewPluginCommand(dockerCli),
system.NewSystemCommand(dockerCli),
trust.NewTrustCommand(dockerCli),
volume.NewVolumeCommand(dockerCli),
// orchestration (swarm) commands
config.NewConfigCommand(dockerCli),
node.NewNodeCommand(dockerCli),
// secret
secret.NewSecretCommand(dockerCli),
// service
service.NewServiceCommand(dockerCli),
// system
system.NewSystemCommand(dockerCli),
system.NewVersionCommand(dockerCli),
// stack
stack.NewStackCommand(dockerCli),
// swarm
swarm.NewSwarmCommand(dockerCli),
// trust
trust.NewTrustCommand(dockerCli),
// volume
volume.NewVolumeCommand(dockerCli),
// context
context.NewContextCommand(dockerCli),
// legacy commands may be hidden
hide(system.NewEventsCommand(dockerCli)),
hide(system.NewInfoCommand(dockerCli)),
hide(system.NewInspectCommand(dockerCli)),
hide(container.NewAttachCommand(dockerCli)),
hide(container.NewCommitCommand(dockerCli)),
hide(container.NewCopyCommand(dockerCli)),
hide(container.NewCreateCommand(dockerCli)),
hide(container.NewDiffCommand(dockerCli)),
hide(container.NewExecCommand(dockerCli)),
hide(container.NewExportCommand(dockerCli)),
hide(container.NewKillCommand(dockerCli)),
hide(container.NewLogsCommand(dockerCli)),
hide(container.NewPauseCommand(dockerCli)),
hide(container.NewPortCommand(dockerCli)),
hide(container.NewPsCommand(dockerCli)),
hide(container.NewRenameCommand(dockerCli)),
hide(container.NewRestartCommand(dockerCli)),
hide(container.NewRmCommand(dockerCli)),
@ -85,13 +114,14 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
hide(container.NewUpdateCommand(dockerCli)),
hide(container.NewWaitCommand(dockerCli)),
hide(image.NewHistoryCommand(dockerCli)),
hide(image.NewImagesCommand(dockerCli)),
hide(image.NewImportCommand(dockerCli)),
hide(image.NewLoadCommand(dockerCli)),
hide(image.NewPullCommand(dockerCli)),
hide(image.NewPushCommand(dockerCli)),
hide(image.NewRemoveCommand(dockerCli)),
hide(image.NewSaveCommand(dockerCli)),
hide(image.NewTagCommand(dockerCli)),
hide(system.NewEventsCommand(dockerCli)),
hide(system.NewInspectCommand(dockerCli)),
)
}

View File

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

View File

@ -1,346 +0,0 @@
package completion
import (
"context"
"errors"
"sort"
"testing"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"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(options container.ListOptions) ([]container.Summary, error)
imageListFunc func(options image.ListOptions) ([]image.Summary, error)
networkListFunc func(ctx context.Context, options network.ListOptions) ([]network.Summary, error)
volumeListFunc func(filter filters.Args) (volume.ListResponse, error)
}
func (c *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]container.Summary, error) {
if c.containerListFunc != nil {
return c.containerListFunc(options)
}
return []container.Summary{}, nil
}
func (c *fakeClient) ImageList(_ context.Context, options image.ListOptions) ([]image.Summary, error) {
if c.imageListFunc != nil {
return c.imageListFunc(options)
}
return []image.Summary{}, nil
}
func (c *fakeClient) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
if c.networkListFunc != nil {
return c.networkListFunc(ctx, options)
}
return []network.Inspect{}, nil
}
func (c *fakeClient) VolumeList(_ context.Context, options volume.ListOptions) (volume.ListResponse, error) {
if c.volumeListFunc != nil {
return c.volumeListFunc(options.Filters)
}
return volume.ListResponse{}, nil
}
func TestCompleteContainerNames(t *testing.T) {
tests := []struct {
doc string
showAll, showIDs bool
filters []func(container.Summary) bool
containers []container.Summary
expOut []string
expOpts container.ListOptions
expDirective cobra.ShellCompDirective
}{
{
doc: "no results",
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "all containers",
showAll: true,
containers: []container.Summary{
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
{ID: "id-a", State: "exited", Names: []string{"/container-a"}},
},
expOut: []string{"container-c", "container-c/link-b", "container-b", "container-a"},
expOpts: container.ListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "all containers with ids",
showAll: true,
showIDs: true,
containers: []container.Summary{
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
{ID: "id-a", State: "exited", Names: []string{"/container-a"}},
},
expOut: []string{"id-c", "container-c", "container-c/link-b", "id-b", "container-b", "id-a", "container-a"},
expOpts: container.ListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "only running containers",
showAll: false,
containers: []container.Summary{
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
},
expOut: []string{"container-c", "container-c/link-b"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with filter",
showAll: true,
filters: []func(container.Summary) bool{
func(container container.Summary) bool { return container.State == "created" },
},
containers: []container.Summary{
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
{ID: "id-a", State: "exited", Names: []string{"/container-a"}},
},
expOut: []string{"container-b"},
expOpts: container.ListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "multiple filters",
showAll: true,
filters: []func(container.Summary) bool{
func(container container.Summary) bool { return container.ID == "id-a" },
func(container container.Summary) bool { return container.State == "created" },
},
containers: []container.Summary{
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
{ID: "id-a", State: "created", Names: []string{"/container-a"}},
},
expOut: []string{"container-a"},
expOpts: container.ListOptions{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(opts container.ListOptions) ([]container.Summary, error) {
assert.Check(t, is.DeepEqual(opts, tc.expOpts, cmpopts.IgnoreUnexported(container.ListOptions{}, filters.Args{})))
if tc.expDirective == cobra.ShellCompDirectiveError {
return nil, errors.New("some error occurred")
}
return 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")
sort.Strings(values)
expected := []string{"ENV_A", "ENV_B"}
assert.Check(t, is.DeepEqual(values, expected))
}
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 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(options image.ListOptions) ([]image.Summary, error) {
if tc.expDirective == cobra.ShellCompDirectiveError {
return nil, errors.New("some error occurred")
}
return 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{
{ID: "nw-c", Name: "network-c"},
{ID: "nw-b", Name: "network-b"},
{ID: "nw-a", Name: "network-a"},
},
expOut: []string{"network-c", "network-b", "network-a"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with error",
expDirective: cobra.ShellCompDirectiveError,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
comp := NetworkNames(fakeCLI{&fakeClient{
networkListFunc: func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
if tc.expDirective == cobra.ShellCompDirectiveError {
return nil, errors.New("some error occurred")
}
return 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 TestCompleteNoComplete(t *testing.T) {
values, directives := NoComplete(nil, nil, "")
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveNoFileComp))
assert.Check(t, is.Len(values, 0))
}
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(filter filters.Args) (volume.ListResponse, error) {
if tc.expDirective == cobra.ShellCompDirectiveError {
return volume.ListResponse{}, errors.New("some error occurred")
}
return volume.ListResponse{Volumes: 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))
})
}
}

View File

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

View File

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

View File

@ -4,13 +4,13 @@ import (
"context"
"fmt"
"io"
"io/ioutil"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types/swarm"
"github.com/moby/sys/sequential"
"github.com/docker/docker/pkg/system"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -35,9 +35,8 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
createOpts.Name = args[0]
createOpts.File = args[1]
return RunConfigCreate(cmd.Context(), dockerCli, createOpts)
return RunConfigCreate(dockerCli, createOpts)
},
ValidArgsFunction: completion.NoComplete,
}
flags := cmd.Flags()
flags.VarP(&createOpts.Labels, "label", "l", "Config labels")
@ -48,10 +47,21 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
}
// RunConfigCreate creates a config with the given options.
func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateOptions) error {
apiClient := dockerCLI.Client()
func RunConfigCreate(dockerCli command.Cli, options CreateOptions) error {
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 {
return errors.Errorf("Error reading content from %q: %v", options.File, err)
}
@ -68,59 +78,11 @@ func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateO
Name: options.TemplateDriver,
}
}
r, err := apiClient.ConfigCreate(ctx, spec)
r, err := client.ConfigCreate(ctx, spec)
if err != nil {
return err
}
_, _ = fmt.Fprintln(dockerCLI.Out(), r.ID)
fmt.Fprintln(dockerCli.Out(), r.ID)
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
}
}

View File

@ -1,11 +1,7 @@
package config
import (
"context"
"errors"
"fmt"
"io"
"os"
"io/ioutil"
"path/filepath"
"reflect"
"strings"
@ -14,6 +10,7 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
@ -24,47 +21,43 @@ const configDataFile = "config-create-with-name.golden"
func TestConfigCreateErrors(t *testing.T) {
testCases := []struct {
args []string
configCreateFunc func(context.Context, swarm.ConfigSpec) (types.ConfigCreateResponse, error)
configCreateFunc func(swarm.ConfigSpec) (types.ConfigCreateResponse, error)
expectedError string
}{
{
args: []string{"too_few"},
expectedError: "requires 2 arguments",
expectedError: "requires exactly 2 arguments",
},
{
args: []string{"too", "many", "arguments"},
expectedError: "requires 2 arguments",
{args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 2 arguments",
},
{
args: []string{"name", filepath.Join("testdata", configDataFile)},
configCreateFunc: func(_ context.Context, configSpec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
return types.ConfigCreateResponse{}, errors.New("error creating config")
configCreateFunc: func(configSpec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
return types.ConfigCreateResponse{}, errors.Errorf("error creating config")
},
expectedError: "error creating config",
},
}
for _, tc := range testCases {
t.Run(tc.expectedError, func(t *testing.T) {
cmd := newConfigCreateCommand(
test.NewFakeCli(&fakeClient{
configCreateFunc: tc.configCreateFunc,
}),
)
cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
})
cmd := newConfigCreateCommand(
test.NewFakeCli(&fakeClient{
configCreateFunc: tc.configCreateFunc,
}),
)
cmd.SetArgs(tc.args)
cmd.SetOut(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestConfigCreateWithName(t *testing.T) {
const name = "config-with-name"
name := "foo"
var actual []byte
cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if spec.Name != name {
return types.ConfigCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name)
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
}
actual = spec.Data
@ -87,9 +80,9 @@ func TestConfigCreateWithLabels(t *testing.T) {
"lbl1": "Label-foo",
"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)
expected := swarm.ConfigSpec{
@ -101,9 +94,9 @@ func TestConfigCreateWithLabels(t *testing.T) {
}
cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if !reflect.DeepEqual(spec, expected) {
return types.ConfigCreateResponse{}, fmt.Errorf("expected %+v, got %+v", expected, spec)
return types.ConfigCreateResponse{}, errors.Errorf("expected %+v, got %+v", expected, spec)
}
return types.ConfigCreateResponse{
@ -124,16 +117,16 @@ func TestConfigCreateWithTemplatingDriver(t *testing.T) {
expectedDriver := &swarm.Driver{
Name: "template-driver",
}
const name = "config-with-template-driver"
name := "foo"
cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if spec.Name != name {
return types.ConfigCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name)
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
}
if spec.Templating.Name != expectedDriver.Name {
return types.ConfigCreateResponse{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
return types.ConfigCreateResponse{}, errors.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
}
return types.ConfigCreateResponse{

View File

@ -5,6 +5,7 @@ import (
"strings"
"time"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/inspect"
"github.com/docker/docker/api/types/swarm"
@ -100,9 +101,9 @@ func (c *configContext) Labels() string {
if mapLabels == nil {
return ""
}
joinLabels := make([]string, 0, len(mapLabels))
var joinLabels []string
for k, v := range mapLabels {
joinLabels = append(joinLabels, k+"="+v)
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
}
return strings.Join(joinLabels, ",")
}
@ -156,11 +157,11 @@ func (ctx *configInspectContext) Labels() map[string]string {
}
func (ctx *configInspectContext) CreatedAt() string {
return formatter.PrettyPrint(ctx.Config.CreatedAt)
return command.PrettyPrint(ctx.Config.CreatedAt)
}
func (ctx *configInspectContext) UpdatedAt() string {
return formatter.PrettyPrint(ctx.Config.UpdatedAt)
return command.PrettyPrint(ctx.Config.UpdatedAt)
}
func (ctx *configInspectContext) Data() string {

View File

@ -19,48 +19,41 @@ func TestConfigContextFormatWrite(t *testing.T) {
// Errors
{
formatter.Context{Format: "{{InvalidFunction}}"},
`template parsing error: template: :1: function "InvalidFunction" not defined`,
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
formatter.Context{Format: "{{nil}}"},
`template parsing error: template: :1:2: executing "" at <nil>: nil is not a command`,
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
// Table format
{
formatter.Context{Format: NewFormat("table", false)},
{formatter.Context{Format: NewFormat("table", false)},
`ID NAME CREATED UPDATED
1 passwords Less than a second ago Less than a second ago
2 id_rsa Less than a second ago Less than a second ago
`,
},
{
formatter.Context{Format: NewFormat("table {{.Name}}", true)},
`},
{formatter.Context{Format: NewFormat("table {{.Name}}", true)},
`NAME
passwords
id_rsa
`,
},
{
formatter.Context{Format: NewFormat("{{.ID}}-{{.Name}}", false)},
`},
{formatter.Context{Format: NewFormat("{{.ID}}-{{.Name}}", false)},
`1-passwords
2-id_rsa
`,
},
`},
}
configs := []swarm.Config{
{
ID: "1",
{ID: "1",
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}},
},
{
ID: "2",
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}}},
{ID: "2",
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}},
},
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}}},
}
for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer
tc.context.Output = &out

View File

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

View File

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

View File

@ -6,9 +6,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter"
flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/fvbommel/sortorder"
@ -31,32 +29,32 @@ func newConfigListCommand(dockerCli command.Cli) *cobra.Command {
Short: "List configs",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return RunConfigList(cmd.Context(), dockerCli, listOpts)
return RunConfigList(dockerCli, listOpts)
},
ValidArgsFunction: completion.NoComplete,
}
flags := cmd.Flags()
flags.BoolVarP(&listOpts.Quiet, "quiet", "q", false, "Only display IDs")
flags.StringVar(&listOpts.Format, "format", "", flagsHelper.FormatHelp)
flags.StringVarP(&listOpts.Format, "format", "", "", "Pretty-print configs using a Go template")
flags.VarP(&listOpts.Filter, "filter", "f", "Filter output based on conditions provided")
return cmd
}
// RunConfigList lists Swarm configs.
func RunConfigList(ctx context.Context, dockerCLI command.Cli, options ListOptions) error {
apiClient := dockerCLI.Client()
func RunConfigList(dockerCli command.Cli, options ListOptions) error {
client := dockerCli.Client()
ctx := context.Background()
configs, err := apiClient.ConfigList(ctx, types.ConfigListOptions{Filters: options.Filter.Value()})
configs, err := client.ConfigList(ctx, types.ConfigListOptions{Filters: options.Filter.Value()})
if err != nil {
return err
}
format := options.Format
if len(format) == 0 {
if len(dockerCLI.ConfigFile().ConfigFormat) > 0 && !options.Quiet {
format = dockerCLI.ConfigFile().ConfigFormat
if len(dockerCli.ConfigFile().ConfigFormat) > 0 && !options.Quiet {
format = dockerCli.ConfigFile().ConfigFormat
} else {
format = formatter.TableFormatKey
}
@ -67,7 +65,7 @@ func RunConfigList(ctx context.Context, dockerCLI command.Cli, options ListOptio
})
configCtx := formatter.Context{
Output: dockerCLI.Out(),
Output: dockerCli.Out(),
Format: NewFormat(format, options.Quiet),
}
return FormatWrite(configCtx, configs)

View File

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

View File

@ -2,11 +2,12 @@ package config
import (
"context"
"errors"
"fmt"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -25,26 +26,30 @@ func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command {
opts := RemoveOptions{
Names: args,
}
return RunConfigRemove(cmd.Context(), dockerCli, opts)
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completeNames(dockerCli)(cmd, args, toComplete)
return RunConfigRemove(dockerCli, opts)
},
}
}
// RunConfigRemove removes the given Swarm configs.
func RunConfigRemove(ctx context.Context, dockerCLI command.Cli, opts RemoveOptions) error {
apiClient := dockerCLI.Client()
func RunConfigRemove(dockerCli command.Cli, opts RemoveOptions) error {
client := dockerCli.Client()
ctx := context.Background()
var errs []string
var errs []error
for _, name := range opts.Names {
if err := apiClient.ConfigRemove(ctx, name); err != nil {
errs = append(errs, err)
if err := client.ConfigRemove(ctx, name); err != nil {
errs = append(errs, err.Error())
continue
}
_, _ = fmt.Fprintln(dockerCLI.Out(), name)
fmt.Fprintln(dockerCli.Out(), name)
}
return errors.Join(errs...)
if len(errs) > 0 {
return errors.Errorf("%s", strings.Join(errs, "\n"))
}
return nil
}

View File

@ -1,12 +1,12 @@
package config
import (
"errors"
"io"
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
@ -19,12 +19,12 @@ func TestConfigRemoveErrors(t *testing.T) {
}{
{
args: []string{},
expectedError: "requires at least 1 argument",
expectedError: "requires at least 1 argument.",
},
{
args: []string{"foo"},
configRemoveFunc: func(name string) error {
return errors.New("error removing config")
return errors.Errorf("error removing config")
},
expectedError: "error removing config",
},
@ -36,8 +36,7 @@ func TestConfigRemoveErrors(t *testing.T) {
}),
)
cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetOut(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
@ -66,7 +65,7 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
configRemoveFunc: func(name string) error {
removedConfigs = append(removedConfigs, name)
if name == "foo" {
return errors.New("error removing config: " + name)
return errors.Errorf("error removing config: %s", name)
}
return nil
},
@ -74,8 +73,7 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
cmd := newConfigRemoveCommand(cli)
cmd.SetArgs(names)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetOut(ioutil.Discard)
assert.Error(t, cmd.Execute(), "error removing config: foo")
assert.Check(t, is.DeepEqual(names, removedConfigs))
}

View File

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

View File

@ -1,13 +1,15 @@
package container
import (
"errors"
"io"
"fmt"
"io/ioutil"
"testing"
"github.com/docker/cli/cli"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
)
@ -16,83 +18,79 @@ func TestNewAttachCommandErrors(t *testing.T) {
name string
args []string
expectedError string
containerInspectFunc func(img string) (container.InspectResponse, error)
containerInspectFunc func(img string) (types.ContainerJSON, error)
}{
{
name: "client-error",
args: []string{"5cb5bb5e4a3b"},
expectedError: "something went wrong",
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
return container.InspectResponse{}, errors.New("something went wrong")
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
return types.ContainerJSON{}, errors.Errorf("something went wrong")
},
},
{
name: "client-stopped",
args: []string{"5cb5bb5e4a3b"},
expectedError: "You cannot attach to a stopped container",
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
return container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
State: &container.State{
Running: false,
},
},
}, nil
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
c := types.ContainerJSON{}
c.ContainerJSONBase = &types.ContainerJSONBase{}
c.ContainerJSONBase.State = &types.ContainerState{Running: false}
return c, nil
},
},
{
name: "client-paused",
args: []string{"5cb5bb5e4a3b"},
expectedError: "You cannot attach to a paused container",
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
return container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
State: &container.State{
Running: true,
Paused: true,
},
},
}, nil
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
c := types.ContainerJSON{}
c.ContainerJSONBase = &types.ContainerJSONBase{}
c.ContainerJSONBase.State = &types.ContainerState{
Running: true,
Paused: true,
}
return c, nil
},
},
{
name: "client-restarting",
args: []string{"5cb5bb5e4a3b"},
expectedError: "You cannot attach to a restarting container",
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
return container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
State: &container.State{
Running: true,
Paused: false,
Restarting: true,
},
},
}, nil
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
c := types.ContainerJSON{}
c.ContainerJSONBase = &types.ContainerJSONBase{}
c.ContainerJSONBase.State = &types.ContainerState{
Running: true,
Paused: false,
Restarting: true,
}
return c, nil
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs(tc.args)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
})
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
cmd.SetOut(ioutil.Discard)
cmd.SetArgs(tc.args)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestGetExitStatus(t *testing.T) {
expectedErr := errors.New("unexpected error")
var (
expectedErr = fmt.Errorf("unexpected error")
errC = make(chan error, 1)
resultC = make(chan container.ContainerWaitOKBody, 1)
)
testcases := []struct {
result *container.WaitResponse
result *container.ContainerWaitOKBody
err error
expectedError error
}{
{
result: &container.WaitResponse{
result: &container.ContainerWaitOKBody{
StatusCode: 0,
},
},
@ -101,13 +99,13 @@ func TestGetExitStatus(t *testing.T) {
expectedError: expectedErr,
},
{
result: &container.WaitResponse{
Error: &container.WaitExitError{Message: expectedErr.Error()},
result: &container.ContainerWaitOKBody{
Error: &container.ContainerWaitOKBodyError{Message: expectedErr.Error()},
},
expectedError: expectedErr,
},
{
result: &container.WaitResponse{
result: &container.ContainerWaitOKBody{
StatusCode: 15,
},
expectedError: cli.StatusError{StatusCode: 15},
@ -115,17 +113,13 @@ func TestGetExitStatus(t *testing.T) {
}
for _, testcase := range testcases {
errC := make(chan error, 1)
resultC := make(chan container.WaitResponse, 1)
if testcase.err != nil {
errC <- testcase.err
}
if testcase.result != nil {
resultC <- *testcase.result
}
err := getExitStatus(errC, resultC)
if testcase.expectedError == nil {
assert.NilError(t, err)
} else {

View File

@ -6,76 +6,64 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/client"
specs "github.com/opencontainers/image-spec/specs-go/v1"
)
type fakeClient struct {
client.Client
inspectFunc func(string) (container.InspectResponse, error)
execInspectFunc func(execID string) (container.ExecInspect, error)
execCreateFunc func(containerID string, options container.ExecOptions) (container.ExecCreateResponse, error)
inspectFunc func(string) (types.ContainerJSON, error)
execInspectFunc func(execID string) (types.ContainerExecInspect, error)
execCreateFunc func(container string, config types.ExecConfig) (types.IDResponse, error)
createContainerFunc func(config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
platform *specs.Platform,
containerName string) (container.CreateResponse, error)
containerStartFunc func(containerID string, options container.StartOptions) error
imageCreateFunc func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error)
infoFunc func() (system.Info, error)
containerStatPathFunc func(containerID, path string) (container.PathStat, error)
containerCopyFromFunc func(containerID, srcPath string) (io.ReadCloser, container.PathStat, error)
logFunc func(string, container.LogsOptions) (io.ReadCloser, error)
waitFunc func(string) (<-chan container.WaitResponse, <-chan error)
containerListFunc func(container.ListOptions) ([]container.Summary, error)
containerName string) (container.ContainerCreateCreatedBody, error)
containerStartFunc func(container string, options types.ContainerStartOptions) error
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
infoFunc func() (types.Info, error)
containerStatPathFunc func(container, path string) (types.ContainerPathStat, error)
containerCopyFromFunc func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error)
logFunc func(string, types.ContainerLogsOptions) (io.ReadCloser, error)
waitFunc func(string) (<-chan container.ContainerWaitOKBody, <-chan error)
containerListFunc func(types.ContainerListOptions) ([]types.Container, error)
containerExportFunc func(string) (io.ReadCloser, error)
containerExecResizeFunc func(id string, options container.ResizeOptions) error
containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error
containerRestartFunc func(ctx context.Context, containerID string, options container.StopOptions) error
containerStopFunc func(ctx context.Context, containerID string, options container.StopOptions) error
containerKillFunc func(ctx context.Context, containerID, signal string) error
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error)
containerAttachFunc func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error)
containerDiffFunc func(ctx context.Context, containerID string) ([]container.FilesystemChange, error)
containerRenameFunc func(ctx context.Context, oldName, newName string) error
containerCommitFunc func(ctx context.Context, container string, options container.CommitOptions) (container.CommitResponse, error)
containerPauseFunc func(ctx context.Context, container string) error
containerExecResizeFunc func(id string, options types.ResizeOptions) error
containerRemoveFunc func(ctx context.Context, container string, options types.ContainerRemoveOptions) error
Version string
}
func (f *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]container.Summary, error) {
func (f *fakeClient) ContainerList(_ context.Context, options types.ContainerListOptions) ([]types.Container, error) {
if f.containerListFunc != nil {
return f.containerListFunc(options)
}
return []container.Summary{}, nil
return []types.Container{}, nil
}
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (container.InspectResponse, error) {
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
if f.inspectFunc != nil {
return f.inspectFunc(containerID)
}
return container.InspectResponse{}, nil
return types.ContainerJSON{}, nil
}
func (f *fakeClient) ContainerExecCreate(_ context.Context, containerID string, config container.ExecOptions) (container.ExecCreateResponse, error) {
func (f *fakeClient) ContainerExecCreate(_ context.Context, container string, config types.ExecConfig) (types.IDResponse, error) {
if f.execCreateFunc != nil {
return f.execCreateFunc(containerID, config)
return f.execCreateFunc(container, config)
}
return container.ExecCreateResponse{}, nil
return types.IDResponse{}, nil
}
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (container.ExecInspect, error) {
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (types.ContainerExecInspect, error) {
if f.execInspectFunc != nil {
return f.execInspectFunc(execID)
}
return container.ExecInspect{}, nil
return types.ContainerExecInspect{}, nil
}
func (*fakeClient) ContainerExecStart(context.Context, string, container.ExecStartOptions) error {
func (f *fakeClient) ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error {
return nil
}
@ -86,51 +74,51 @@ func (f *fakeClient) ContainerCreate(
networkingConfig *network.NetworkingConfig,
platform *specs.Platform,
containerName string,
) (container.CreateResponse, error) {
) (container.ContainerCreateCreatedBody, error) {
if f.createContainerFunc != nil {
return f.createContainerFunc(config, hostConfig, networkingConfig, platform, containerName)
}
return container.CreateResponse{}, nil
return container.ContainerCreateCreatedBody{}, nil
}
func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, options container.RemoveOptions) error {
func (f *fakeClient) ContainerRemove(ctx context.Context, container string, options types.ContainerRemoveOptions) error {
if f.containerRemoveFunc != nil {
return f.containerRemoveFunc(ctx, containerID, options)
return f.containerRemoveFunc(ctx, container, options)
}
return nil
}
func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
if f.imageCreateFunc != nil {
return f.imageCreateFunc(ctx, parentReference, options)
return f.imageCreateFunc(parentReference, options)
}
return nil, nil
}
func (f *fakeClient) Info(_ context.Context) (system.Info, error) {
func (f *fakeClient) Info(_ context.Context) (types.Info, error) {
if f.infoFunc != nil {
return f.infoFunc()
}
return system.Info{}, nil
return types.Info{}, nil
}
func (f *fakeClient) ContainerStatPath(_ context.Context, containerID, path string) (container.PathStat, error) {
func (f *fakeClient) ContainerStatPath(_ context.Context, container, path string) (types.ContainerPathStat, error) {
if f.containerStatPathFunc != nil {
return f.containerStatPathFunc(containerID, path)
return f.containerStatPathFunc(container, path)
}
return container.PathStat{}, nil
return types.ContainerPathStat{}, nil
}
func (f *fakeClient) CopyFromContainer(_ context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
func (f *fakeClient) CopyFromContainer(_ context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
if f.containerCopyFromFunc != nil {
return f.containerCopyFromFunc(containerID, srcPath)
return f.containerCopyFromFunc(container, srcPath)
}
return nil, container.PathStat{}, nil
return nil, types.ContainerPathStat{}, nil
}
func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
func (f *fakeClient) ContainerLogs(_ context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
if f.logFunc != nil {
return f.logFunc(containerID, options)
return f.logFunc(container, options)
}
return nil, nil
}
@ -139,96 +127,30 @@ func (f *fakeClient) ClientVersion() string {
return f.Version
}
func (f *fakeClient) ContainerWait(_ context.Context, containerID string, _ container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
func (f *fakeClient) ContainerWait(_ context.Context, container string, _ container.WaitCondition) (<-chan container.ContainerWaitOKBody, <-chan error) {
if f.waitFunc != nil {
return f.waitFunc(containerID)
return f.waitFunc(container)
}
return nil, nil
}
func (f *fakeClient) ContainerStart(_ context.Context, containerID string, options container.StartOptions) error {
func (f *fakeClient) ContainerStart(_ context.Context, container string, options types.ContainerStartOptions) error {
if f.containerStartFunc != nil {
return f.containerStartFunc(containerID, options)
return f.containerStartFunc(container, options)
}
return nil
}
func (f *fakeClient) ContainerExport(_ context.Context, containerID string) (io.ReadCloser, error) {
func (f *fakeClient) ContainerExport(_ context.Context, container string) (io.ReadCloser, error) {
if f.containerExportFunc != nil {
return f.containerExportFunc(containerID)
return f.containerExportFunc(container)
}
return nil, nil
}
func (f *fakeClient) ContainerExecResize(_ context.Context, id string, options container.ResizeOptions) error {
func (f *fakeClient) ContainerExecResize(_ context.Context, id string, options types.ResizeOptions) error {
if f.containerExecResizeFunc != nil {
return f.containerExecResizeFunc(id, options)
}
return nil
}
func (f *fakeClient) ContainerKill(ctx context.Context, containerID, signal string) error {
if f.containerKillFunc != nil {
return f.containerKillFunc(ctx, containerID, signal)
}
return nil
}
func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
if f.containerPruneFunc != nil {
return f.containerPruneFunc(ctx, pruneFilters)
}
return container.PruneReport{}, nil
}
func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error {
if f.containerRestartFunc != nil {
return f.containerRestartFunc(ctx, containerID, options)
}
return nil
}
func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error {
if f.containerStopFunc != nil {
return f.containerStopFunc(ctx, containerID, options)
}
return nil
}
func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
if f.containerAttachFunc != nil {
return f.containerAttachFunc(ctx, containerID, options)
}
return types.HijackedResponse{}, nil
}
func (f *fakeClient) ContainerDiff(ctx context.Context, containerID string) ([]container.FilesystemChange, error) {
if f.containerDiffFunc != nil {
return f.containerDiffFunc(ctx, containerID)
}
return []container.FilesystemChange{}, nil
}
func (f *fakeClient) ContainerRename(ctx context.Context, oldName, newName string) error {
if f.containerRenameFunc != nil {
return f.containerRenameFunc(ctx, oldName, newName)
}
return nil
}
func (f *fakeClient) ContainerCommit(ctx context.Context, containerID string, options container.CommitOptions) (container.CommitResponse, error) {
if f.containerCommitFunc != nil {
return f.containerCommitFunc(ctx, containerID, options)
}
return container.CommitResponse{}, nil
}
func (f *fakeClient) ContainerPause(ctx context.Context, containerID string) error {
if f.containerPauseFunc != nil {
return f.containerPauseFunc(ctx, containerID)
}
return nil
}

View File

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

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