Compare commits
276 Commits
fix-multip
...
v20.10.20
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fdeb9c3de | |||
| a12c535f6e | |||
| d18a3e9004 | |||
| 932ca73874 | |||
| 7d51e65e72 | |||
| d85ef84533 | |||
| d1f9546dc3 | |||
| 1ea8d69d6f | |||
| e82aa85741 | |||
| e9176b36cc | |||
| a39f3fbdfd | |||
| 2fa0c6253a | |||
| bc6ff39e42 | |||
| 3fa7a8654f | |||
| 18e275c52e | |||
| 3e06ce8bfa | |||
| b40c2f6b5d | |||
| 93eead45ee | |||
| bd04f199f7 | |||
| 2484f7e046 | |||
| 45075ea08c | |||
| c2dcaecf19 | |||
| 613b9362d0 | |||
| b30d250320 | |||
| fe0cdaf027 | |||
| 6b25bc3003 | |||
| 0785bd743d | |||
| bdac0b38d9 | |||
| c70b01ec1f | |||
| 0389090aeb | |||
| c904936d69 | |||
| 386d50c2e9 | |||
| 990186f2f6 | |||
| 86bf1966e2 | |||
| b3022b91d1 | |||
| f14ba9f5d7 | |||
| ae45936575 | |||
| c189c4dbea | |||
| 0c46ffc1f9 | |||
| 6be9ce798e | |||
| c8b7ef1e29 | |||
| 779ed309a8 | |||
| 2f7e84be65 | |||
| e628209d9b | |||
| 80a3add604 | |||
| 715cfc4c2f | |||
| 80fb0d575e | |||
| 9d28e08a2d | |||
| d72bef2088 | |||
| 100c70180f | |||
| 7502d7e560 | |||
| 4d718932c1 | |||
| 826eaafa6d | |||
| 308624c3b1 | |||
| de7d866b6a | |||
| aa7e414fdc | |||
| 240e4b5501 | |||
| 5d4776bd90 | |||
| 1841277463 | |||
| 49e9c2ae3d | |||
| 87a3ce2699 | |||
| 1d8abed17d | |||
| fd82621d35 | |||
| e5ca16bda7 | |||
| 31dad66f9a | |||
| 113c5b526a | |||
| c73edb36ca | |||
| 80f673bf9e | |||
| 3d4cc8e699 | |||
| 30277a8f80 | |||
| 994ed4085b | |||
| b10a23a46f | |||
| 26d906094a | |||
| 9c091c4346 | |||
| a13500b745 | |||
| cfef3a7dc1 | |||
| 53426025c3 | |||
| 573a664639 | |||
| cf0ab7ac4c | |||
| d05fd4ffc8 | |||
| 870f138250 | |||
| 7ffc243093 | |||
| 198d6b8724 | |||
| 55a14ec851 | |||
| 1f9a0df05a | |||
| 4ae338b33a | |||
| 6380142dd4 | |||
| 82f422fcf3 | |||
| 80fd77903b | |||
| c3d4d623c8 | |||
| 2e82d11def | |||
| 738a6ee1cc | |||
| 246d96bb6c | |||
| 2fd0f17057 | |||
| 5fa500000a | |||
| 1e6a8ce2b7 | |||
| 81d965569d | |||
| 6f7a931a2d | |||
| 16169434a4 | |||
| 91bab605f7 | |||
| a224086349 | |||
| a282e0c5d2 | |||
| f3764ff5f9 | |||
| 700364e304 | |||
| 62d27c32ff | |||
| c0e952cf04 | |||
| 04104a04d3 | |||
| b721998b7b | |||
| 4065e1246e | |||
| f1002eb9fb | |||
| e97c7b240e | |||
| aa78937634 | |||
| 40fe0573aa | |||
| c9737e1c37 | |||
| 5c6723d080 | |||
| 59a8a0906f | |||
| 3f9857e6a8 | |||
| fd5fc61ecd | |||
| 3624019d83 | |||
| 96f2cf80ab | |||
| f3ff8e6ad6 | |||
| ee1ac1b319 | |||
| e91ed5707e | |||
| 38dd744a11 | |||
| cedc602a78 | |||
| 4de40a825e | |||
| dea9396e18 | |||
| 03fa8f92c8 | |||
| b485636f4b | |||
| 241523cc1a | |||
| 9989fdbc40 | |||
| 0e20c1fd21 | |||
| c4cb29837f | |||
| 0b56256638 | |||
| 1c0927a041 | |||
| 82f9d5921b | |||
| adb01ca79d | |||
| 8260476a06 | |||
| aa5b6b7728 | |||
| 859e303655 | |||
| bce2e1f953 | |||
| 44064f51c8 | |||
| 292779add5 | |||
| f2e79b826c | |||
| fa46b92361 | |||
| 400f81089a | |||
| c72057c8db | |||
| 77db97d595 | |||
| cbf0d2b7b7 | |||
| d0014a86bc | |||
| 6c1c8b55aa | |||
| c2ea9bc90b | |||
| 44fdac11f5 | |||
| 893e52cf4b | |||
| 061051c24d | |||
| 62eae52c2a | |||
| 2012fbf111 | |||
| 42d1c02750 | |||
| 3967b7d28e | |||
| 0b924e51fc | |||
| 6288e8b1ac | |||
| 1e9575e81a | |||
| c98e9c47ca | |||
| a6f6b5fa34 | |||
| 8437cfefae | |||
| 68a5ca859f | |||
| f9d091f4b1 | |||
| e9b8231d6a | |||
| 8a64739631 | |||
| a555c853b0 | |||
| 260ba1a8a2 | |||
| f63cb8b97e | |||
| c1492eabde | |||
| 48e6b44379 | |||
| bfcd17b5b7 | |||
| 8279b718ea | |||
| 644c003606 | |||
| 0d17280a30 | |||
| eedfe50a99 | |||
| f3dd1ee6c1 | |||
| c7cf60f657 | |||
| 1d37fb3027 | |||
| 0793f96394 | |||
| b639ea8b89 | |||
| 063e3dd329 | |||
| 0168626037 | |||
| 00ea8bdc41 | |||
| e3a9a92b14 | |||
| 6da4ee40c7 | |||
| ab733b5564 | |||
| f0df35096d | |||
| f485f66943 | |||
| 746c553574 | |||
| 2945ba4f7a | |||
| 032e485e1c | |||
| 88de81ff21 | |||
| 706ca7985b | |||
| e0d47b1c0b | |||
| 54b529feae | |||
| c88e6432ec | |||
| f291a49ba5 | |||
| 78fcd905c6 | |||
| 12e2f94eba | |||
| 00755d7dba | |||
| 8264f5be8d | |||
| 9780f41efe | |||
| 4fbdf3f362 | |||
| 1ff45aac40 | |||
| bb03b9d3c2 | |||
| ed71df1b9f | |||
| ee20fa1ec4 | |||
| ffe40dc6b6 | |||
| 7ab2d19a1e | |||
| 4630fe0075 | |||
| fbbf1be52d | |||
| 3de2cc6efd | |||
| 234036d105 | |||
| 0c442dc179 | |||
| 6b48c78672 | |||
| 370c28948e | |||
| dc017bdda3 | |||
| feb6f439e3 | |||
| 8bc4062fc0 | |||
| 84cc7d87cc | |||
| c1c3d3b3aa | |||
| 048a846146 | |||
| 33dacda24f | |||
| fcc05e5ea1 | |||
| 58061d25f6 | |||
| 55b2abb0ca | |||
| 4c3b87d922 | |||
| e77605c83d | |||
| 0196098721 | |||
| 6ebf765040 | |||
| f508ce9db7 | |||
| 897293c7c7 | |||
| 2c04354315 | |||
| 9c8a91d22b | |||
| ff945151ef | |||
| 4571d90f20 | |||
| 55c4c88966 | |||
| f33a69f6ee | |||
| d3cb89ee53 | |||
| 433014bcfb | |||
| 61cb016851 | |||
| 53c4602909 | |||
| 830d6595e7 | |||
| 13048444cc | |||
| 3e293e6bd1 | |||
| 41b3ea7e47 | |||
| d6eeeb6259 | |||
| 3e157d5293 | |||
| 1fdf84b8e9 | |||
| 376b99c6d6 | |||
| 0de4e6e9a7 | |||
| 3c87f01b18 | |||
| de40c2b172 | |||
| d513e46bfc | |||
| 2b74b90efb | |||
| 05343b36a2 | |||
| f90db254d7 | |||
| 0dcfdde336 | |||
| 03cd1dc50a | |||
| 42811a7eb9 | |||
| be966aa194 | |||
| b22fe0fb14 | |||
| 4eb050071e | |||
| 08c4fdfa7a | |||
| 6aa1b37c8d | |||
| e82920d76d | |||
| 82123939f7 | |||
| 48d30b5b32 | |||
| 5941f4104a | |||
| 18f33b337d | |||
| 9ecc69d17e | |||
| 6f49197cab |
153
.circleci/config.yml
Normal file
153
.circleci/config.yml
Normal file
@ -0,0 +1,153 @@
|
||||
version: 2
|
||||
|
||||
jobs:
|
||||
|
||||
lint:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:20.10-git'}]
|
||||
environment:
|
||||
DOCKER_BUILDKIT: 1
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 20.10.6
|
||||
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:20.10-git'}]
|
||||
environment:
|
||||
DOCKER_BUILDKIT: 1
|
||||
BUILDX_VERSION: "v0.8.2"
|
||||
parallelism: 3
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 20.10.6
|
||||
reusable: true
|
||||
exclusive: false
|
||||
- run:
|
||||
name: "Docker version"
|
||||
command: docker version
|
||||
- run:
|
||||
name: "Docker info"
|
||||
command: docker info
|
||||
- run: apk add make curl
|
||||
- run: mkdir -vp ~/.docker/cli-plugins/
|
||||
- run: curl -fsSL --output ~/.docker/cli-plugins/docker-buildx https://github.com/docker/buildx/releases/download/${BUILDX_VERSION}/buildx-${BUILDX_VERSION}.linux-amd64
|
||||
- run: chmod a+x ~/.docker/cli-plugins/docker-buildx
|
||||
- run: docker buildx version
|
||||
- run: docker context create buildctx
|
||||
- run: docker buildx create --use buildctx && docker buildx inspect --bootstrap
|
||||
- run: GROUP_INDEX=$CIRCLE_NODE_INDEX GROUP_TOTAL=$CIRCLE_NODE_TOTAL docker buildx bake cross --progress=plain
|
||||
- store_artifacts:
|
||||
path: /work/build
|
||||
|
||||
test:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:20.10-git'}]
|
||||
environment:
|
||||
DOCKER_BUILDKIT: 1
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 20.10.6
|
||||
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:20.10-git'}]
|
||||
environment:
|
||||
DOCKER_BUILDKIT: 1
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 20.10.6
|
||||
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
|
||||
20
.codecov.yml
20
.codecov.yml
@ -1,20 +0,0 @@
|
||||
comment:
|
||||
layout: header, changes, diff, sunburst
|
||||
coverage:
|
||||
status:
|
||||
patch:
|
||||
default:
|
||||
target: 50%
|
||||
only_pulls: true
|
||||
# project will give us the diff in the total code coverage between a commit
|
||||
# and its parent
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: "15%"
|
||||
changes: false
|
||||
ignore:
|
||||
- "**/internal/test"
|
||||
- "vendor/*"
|
||||
- "cli/compose/schema/bindata.go"
|
||||
- ".*generated.*"
|
||||
@ -1,14 +1,7 @@
|
||||
/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
|
||||
.github
|
||||
.gitignore
|
||||
appveyor.yml
|
||||
build
|
||||
/vndr.log
|
||||
|
||||
14
.gitattributes
vendored
14
.gitattributes
vendored
@ -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
7
.github/CODEOWNERS
vendored
@ -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
64
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
<!--
|
||||
If you are reporting a new issue, make sure that we do not have any duplicates
|
||||
already open. You can ensure this by searching the issue list for this
|
||||
repository. If there is a duplicate, please close your issue and add a comment
|
||||
to the existing issue instead.
|
||||
|
||||
If you suspect your issue is a bug, please edit your issue description to
|
||||
include the BUG REPORT INFORMATION shown below. If you fail to provide this
|
||||
information within 7 days, we cannot debug your issue and will close it. We
|
||||
will, however, reopen it if you later provide the information.
|
||||
|
||||
For more information about reporting issues, see
|
||||
https://github.com/docker/cli/blob/master/CONTRIBUTING.md#reporting-other-issues
|
||||
|
||||
---------------------------------------------------
|
||||
GENERAL SUPPORT INFORMATION
|
||||
---------------------------------------------------
|
||||
|
||||
The GitHub issue tracker is for bug reports and feature requests.
|
||||
General support can be found at the following locations:
|
||||
|
||||
- Docker Support Forums - https://forums.docker.com
|
||||
- Docker Community Slack - https://dockr.ly/community
|
||||
- Post a question on StackOverflow, using the Docker tag
|
||||
|
||||
---------------------------------------------------
|
||||
BUG REPORT INFORMATION
|
||||
---------------------------------------------------
|
||||
Use the commands below to provide key information from your environment:
|
||||
You do NOT have to include this information if this is a FEATURE REQUEST
|
||||
-->
|
||||
|
||||
**Description**
|
||||
|
||||
<!--
|
||||
Briefly describe the problem you are having in a few paragraphs.
|
||||
-->
|
||||
|
||||
**Steps to reproduce the issue:**
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
**Describe the results you received:**
|
||||
|
||||
|
||||
**Describe the results you expected:**
|
||||
|
||||
|
||||
**Additional information you deem important (e.g. issue happens only occasionally):**
|
||||
|
||||
**Output of `docker version`:**
|
||||
|
||||
```
|
||||
(paste your output here)
|
||||
```
|
||||
|
||||
**Output of `docker info`:**
|
||||
|
||||
```
|
||||
(paste your output here)
|
||||
```
|
||||
|
||||
**Additional environment details (AWS, VirtualBox, physical, etc.):**
|
||||
146
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
146
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -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
|
||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
11
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -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/"
|
||||
13
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
13
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -1,13 +0,0 @@
|
||||
name: Feature request
|
||||
description: Missing functionality? Come tell us about it!
|
||||
labels:
|
||||
- kind/feature
|
||||
- status/0-triage
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: What is the feature you want to see?
|
||||
validations:
|
||||
required: true
|
||||
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -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)**
|
||||
|
||||
|
||||
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@ -1,9 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- "area/testing"
|
||||
- "status/2-code-review"
|
||||
175
.github/workflows/build.yml
vendored
175
.github/workflows/build.yml
vendored
@ -1,175 +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@v6
|
||||
-
|
||||
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@v5
|
||||
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=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
-
|
||||
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@v6
|
||||
-
|
||||
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
80
.github/workflows/codeql-analysis.yml
vendored
Normal 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
|
||||
79
.github/workflows/codeql.yml
vendored
79
.github/workflows/codeql.yml
vendored
@ -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@v6
|
||||
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@v6
|
||||
with:
|
||||
go-version: "1.25.5"
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: go
|
||||
-
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
-
|
||||
name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:go"
|
||||
80
.github/workflows/e2e.yml
vendored
80
.github/workflows/e2e.yml
vendored
@ -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:
|
||||
- rc # latest rc
|
||||
- 29 # latest
|
||||
- 28 # latest - 1
|
||||
- 25 # mirantis lts
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
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 }}
|
||||
84
.github/workflows/test.yml
vendored
84
.github/workflows/test.yml
vendored
@ -1,84 +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-14 # macOS 14 on arm64 (Apple Silicon M1)
|
||||
- macos-15-intel # macOS 15 on Intel
|
||||
- macos-15 # macOS 15 on arm64 (Apple Silicon M1)
|
||||
# - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.5"
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust')
|
||||
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 }}
|
||||
93
.github/workflows/validate-pr.yml
vendored
93
.github/workflows/validate-pr.yml
vendored
@ -1,93 +0,0 @@
|
||||
name: validate-pr
|
||||
|
||||
# Default to 'contents: read', which grants actions to read commits.
|
||||
#
|
||||
# If any permission is set, any permission not included in the list is
|
||||
# implicitly set to "none".
|
||||
#
|
||||
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||
|
||||
jobs:
|
||||
check-labels:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
steps:
|
||||
- name: Missing `area/` label
|
||||
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
|
||||
run: |
|
||||
echo "::error::Every PR with an 'impact/*' label should also have an 'area/*' label"
|
||||
exit 1
|
||||
- name: Missing `kind/` label
|
||||
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'kind/')
|
||||
run: |
|
||||
echo "::error::Every PR with an 'impact/*' label should also have a 'kind/*' label"
|
||||
exit 1
|
||||
- name: OK
|
||||
run: exit 0
|
||||
|
||||
check-changelog:
|
||||
runs-on: ubuntu-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
|
||||
82
.github/workflows/validate.yml
vendored
82
.github/workflows/validate.yml
vendored
@ -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@v6
|
||||
-
|
||||
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@v6
|
||||
-
|
||||
name: Run
|
||||
shell: 'script --return --quiet --command "bash {0}"'
|
||||
run: |
|
||||
make -f docker.Makefile ${{ matrix.target }}
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
# if you want to ignore files created by your editor/tools,
|
||||
# consider a global .gitignore https://help.github.com/articles/ignoring-files
|
||||
# please consider a global .gitignore https://help.github.com/articles/ignoring-files
|
||||
*.exe
|
||||
*.exe~
|
||||
*.orig
|
||||
@ -8,10 +8,11 @@
|
||||
Thumbs.db
|
||||
.editorconfig
|
||||
/build/
|
||||
/cmd/docker/winresources/versioninfo.json
|
||||
/cmd/docker/winresources/*.syso
|
||||
cli/winresources/rsrc_*.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
|
||||
|
||||
297
.golangci.yml
297
.golangci.yml
@ -1,236 +1,117 @@
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
# prevent golangci-lint from deducting the go version to lint for through go.mod,
|
||||
# which causes it to fallback to go1.17 semantics.
|
||||
#
|
||||
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
|
||||
go: "1.25.5"
|
||||
|
||||
timeout: 5m
|
||||
|
||||
issues:
|
||||
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
||||
max-issues-per-linter: 0
|
||||
|
||||
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
||||
max-same-issues: 0
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofumpt # Detects whether code was gofumpt-ed.
|
||||
- goimports
|
||||
|
||||
exclusions:
|
||||
generated: strict
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- asasalint # Detects "[]any" used as argument for variadic "func(...any)".
|
||||
- bodyclose
|
||||
- copyloopvar # Detects places where loop variables are copied.
|
||||
- depguard
|
||||
- dogsled # Detects assignments with too many blank identifiers.
|
||||
- dupword # Detects duplicate words.
|
||||
- durationcheck # Detect cases where two time.Duration values are being multiplied in possibly erroneous ways.
|
||||
- errcheck
|
||||
- errchkjson # Detects unsupported types passed to json encoding functions and reports if checks for the returned error can be omitted.
|
||||
- exhaustive # Detects missing options in enum switch statements.
|
||||
- exptostd # Detects functions from golang.org/x/exp/ that can be replaced by std functions.
|
||||
- fatcontext # Detects nested contexts in loops and function literals.
|
||||
- forbidigo
|
||||
- gocheckcompilerdirectives # Detects invalid go compiler directive comments (//go:).
|
||||
- gocritic # Metalinter; detects bugs, performance, and styling issues.
|
||||
- deadcode
|
||||
- dogsled
|
||||
- gocyclo
|
||||
- gosec # Detects security problems.
|
||||
- goimports
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- iface # Detects incorrect use of interfaces. Currently only used for "identical" interfaces in the same package.
|
||||
- importas # Enforces consistent import aliases.
|
||||
- ineffassign
|
||||
- makezero # Finds slice declarations with non-zero initial length.
|
||||
- mirror # Detects wrong mirror patterns of bytes/strings usage.
|
||||
- misspell # Detects commonly misspelled English words in comments.
|
||||
- nakedret # Detects uses of naked returns.
|
||||
- nilnesserr # Detects returning nil errors. It combines the features of nilness and nilerr,
|
||||
- nosprintfhostport # Detects misuse of Sprintf to construct a host with port in a URL.
|
||||
- nolintlint # Detects ill-formed or insufficient nolint directives.
|
||||
- perfsprint # Detects fmt.Sprintf uses that can be replaced with a faster alternative.
|
||||
- prealloc # Detects slice declarations that could potentially be pre-allocated.
|
||||
- predeclared # Detects code that shadows one of Go's predeclared identifiers
|
||||
- reassign # Detects reassigning a top-level variable in another package.
|
||||
- revive # Metalinter; drop-in replacement for golint.
|
||||
- spancheck # Detects mistakes with OpenTelemetry/Census spans.
|
||||
- lll
|
||||
- megacheck
|
||||
- misspell
|
||||
- nakedret
|
||||
- staticcheck
|
||||
- thelper # Detects test helpers without t.Helper().
|
||||
- tparallel # Detects inappropriate usage of t.Parallel().
|
||||
- unconvert # Detects unnecessary type conversions.
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- usestdlibvars # Detects the possibility to use variables/constants from the Go standard library.
|
||||
- usetesting # Reports uses of functions with replacement inside the testing package.
|
||||
- wastedassign # Detects wasted assignment statements.
|
||||
- revive
|
||||
- varcheck
|
||||
|
||||
disable:
|
||||
- errcheck
|
||||
|
||||
settings:
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
deny:
|
||||
- pkg: "github.com/containerd/containerd/errdefs"
|
||||
desc: The containerd errdefs package was migrated to a separate module. Use github.com/containerd/errdefs instead.
|
||||
- pkg: "github.com/containerd/containerd/log"
|
||||
desc: The containerd log package was migrated to a separate module. Use github.com/containerd/log instead.
|
||||
- pkg: "github.com/containerd/containerd/pkg/userns"
|
||||
desc: Use github.com/moby/sys/userns instead.
|
||||
- pkg: "github.com/containerd/containerd/platforms"
|
||||
desc: The containerd platforms package was migrated to a separate module. Use github.com/containerd/platforms instead.
|
||||
- pkg: "github.com/docker/docker/errdefs"
|
||||
desc: Use github.com/containerd/errdefs instead.
|
||||
- pkg: "github.com/docker/docker/pkg/system"
|
||||
desc: This package should not be used unless strictly necessary.
|
||||
- pkg: "github.com/docker/distribution/uuid"
|
||||
desc: Use github.com/google/uuid instead.
|
||||
- pkg: "io/ioutil"
|
||||
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
|
||||
run:
|
||||
timeout: 5m
|
||||
skip-dirs:
|
||||
- cli/command/stack/kubernetes/api/openapi
|
||||
- cli/command/stack/kubernetes/api/client
|
||||
skip-files:
|
||||
- cli/compose/schema/bindata.go
|
||||
- .*generated.*
|
||||
|
||||
forbidigo:
|
||||
forbid:
|
||||
- pkg: ^regexp$
|
||||
pattern: ^regexp\.MustCompile
|
||||
msg: Use internal/lazyregexp.New instead.
|
||||
linters-settings:
|
||||
gocyclo:
|
||||
min-complexity: 16
|
||||
govet:
|
||||
check-shadowing: false
|
||||
lll:
|
||||
line-length: 200
|
||||
nakedret:
|
||||
command: nakedret
|
||||
pattern: ^(?P<path>.*?\\.go):(?P<line>\\d+)\\s*(?P<message>.*)$
|
||||
|
||||
gocyclo:
|
||||
min-complexity: 16
|
||||
issues:
|
||||
# The default exclusion rules are a bit too permissive, so copying the relevant ones below
|
||||
exclude-use-default: false
|
||||
|
||||
gosec:
|
||||
excludes:
|
||||
- G104 # G104: Errors unhandled; (TODO: reduce unhandled errors, or explicitly ignore)
|
||||
- G115 # G115: integer overflow conversion; (TODO: verify these: https://github.com/docker/cli/issues/5584)
|
||||
- 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")
|
||||
exclude:
|
||||
- parameter .* always receives
|
||||
|
||||
govet:
|
||||
enable:
|
||||
- shadow
|
||||
settings:
|
||||
shadow:
|
||||
strict: true
|
||||
|
||||
lll:
|
||||
line-length: 200
|
||||
|
||||
importas:
|
||||
# Do not allow unaliased imports of aliased packages.
|
||||
no-unaliased: true
|
||||
|
||||
alias:
|
||||
# Should no longer be aliased, because we no longer allow moby/docker errdefs.
|
||||
- pkg: "github.com/docker/docker/errdefs"
|
||||
alias: ""
|
||||
- pkg: github.com/opencontainers/image-spec/specs-go/v1
|
||||
alias: ocispec
|
||||
# Enforce that gotest.tools/v3/assert/cmp is always aliased as "is"
|
||||
- pkg: gotest.tools/v3/assert/cmp
|
||||
alias: is
|
||||
|
||||
nakedret:
|
||||
# Disallow naked returns if func has more lines of code than this setting.
|
||||
# Default: 30
|
||||
max-func-lines: 0
|
||||
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
- -QF1008 # Omit embedded fields from selector expression; https://staticcheck.dev/docs/checks/#QF1008
|
||||
|
||||
revive:
|
||||
rules:
|
||||
- name: empty-block # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block
|
||||
- name: empty-lines # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
|
||||
- name: import-shadowing # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing
|
||||
- name: line-length-limit # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#line-length-limit
|
||||
arguments: [200]
|
||||
- name: unused-receiver # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver
|
||||
- name: use-any # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
|
||||
- name: use-errors-new # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#use-errors-new
|
||||
|
||||
usetesting:
|
||||
os-chdir: false # FIXME(thaJeztah): Disable `os.Chdir()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
|
||||
context-background: 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-todo: 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
|
||||
|
||||
exclusions:
|
||||
# We prefer to use an "linters.exclusions.rules" so that new "default" exclusions are not
|
||||
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 excludes at:
|
||||
# https://github.com/golangci/golangci-lint/blob/v1.61.0/pkg/config/issues.go#L11-L104
|
||||
#
|
||||
# The default list of exclusions can be found at:
|
||||
# https://golangci-lint.run/usage/false-positives/#default-exclusions
|
||||
generated: strict
|
||||
# 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
|
||||
|
||||
rules:
|
||||
# EXC0003
|
||||
- text: "func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this"
|
||||
linters:
|
||||
- revive
|
||||
# EXC0001
|
||||
- text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv). is not checked"
|
||||
linters:
|
||||
- errcheck
|
||||
# EXC0003
|
||||
- text: "func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this"
|
||||
linters:
|
||||
- revive
|
||||
# EXC0006
|
||||
- text: "Use of unsafe calls should be audited"
|
||||
linters:
|
||||
- gosec
|
||||
# EXC0007
|
||||
- text: "Subprocess launch(ed with variable|ing should be audited)"
|
||||
linters:
|
||||
- gosec
|
||||
# EXC0008
|
||||
# TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close" (gosec)
|
||||
- text: "(G104|G307)"
|
||||
linters:
|
||||
- gosec
|
||||
# EXC0009
|
||||
- text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
|
||||
linters:
|
||||
- gosec
|
||||
# EXC0010
|
||||
- text: "Potential file inclusion via variable"
|
||||
linters:
|
||||
- gosec
|
||||
|
||||
# EXC0007
|
||||
- text: "Subprocess launch(ed with variable|ing should be audited)"
|
||||
linters:
|
||||
- gosec
|
||||
# Looks like the match in "EXC0007" above doesn't catch this one
|
||||
# TODO: consider upstreaming this to golangci-lint's default exclusion rules
|
||||
- text: "G204: Subprocess launched with a potential tainted input or cmd arguments"
|
||||
linters:
|
||||
- gosec
|
||||
# Looks like the match in "EXC0009" above doesn't catch this one
|
||||
# TODO: consider upstreaming this to golangci-lint's default exclusion rules
|
||||
- text: "G306: Expect WriteFile permissions to be 0600 or less"
|
||||
linters:
|
||||
- gosec
|
||||
|
||||
# EXC0009
|
||||
- text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
|
||||
linters:
|
||||
- gosec
|
||||
# Exclude some linters from running on tests files.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- errcheck
|
||||
- gosec
|
||||
|
||||
# EXC0010
|
||||
- text: "Potential file inclusion via variable"
|
||||
linters:
|
||||
- gosec
|
||||
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
||||
max-issues-per-linter: 0
|
||||
|
||||
# TODO: make sure all packages have a description. Currently, there's 67 packages without.
|
||||
- text: "package-comments: should have a package comment"
|
||||
linters:
|
||||
- revive
|
||||
|
||||
# Exclude some linters from running on tests files.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- errcheck
|
||||
- gosec
|
||||
|
||||
- text: "ST1000: at least one file in a package should have a package comment"
|
||||
linters:
|
||||
- staticcheck
|
||||
|
||||
# Allow "err" and "ok" vars to shadow existing declarations, otherwise we get too many false positives.
|
||||
- text: '^shadow: declaration of "(err|ok)" shadows declaration'
|
||||
linters:
|
||||
- govet
|
||||
|
||||
# Ignore for cli/command/formatter/tabwriter, which is forked from go stdlib, so we want to align with it.
|
||||
- text: '^(ST1020|ST1022): comment on exported'
|
||||
path: "cli/command/formatter/tabwriter"
|
||||
linters:
|
||||
- staticcheck
|
||||
|
||||
# Ignore deprecation linting for cli/command/stack/*.
|
||||
#
|
||||
# FIXME(thaJeztah): remove exception once these functions are un-exported or internal; see https://github.com/docker/cli/pull/6389
|
||||
- text: '^(SA1019): '
|
||||
path: "cli/command/stack"
|
||||
linters:
|
||||
- staticcheck
|
||||
|
||||
# Log a warning if an exclusion rule is unused.
|
||||
# Default: false
|
||||
warn-unused: true
|
||||
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
||||
max-same-issues: 0
|
||||
|
||||
118
.mailmap
118
.mailmap
@ -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,16 +26,13 @@ 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>
|
||||
Allie Sadler <allie.sadler@docker.com>
|
||||
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@microsoft.com>
|
||||
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@outlook.com>
|
||||
André Martins <aanm90@gmail.com> <martins@noironetworks.com>
|
||||
@ -59,24 +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>
|
||||
Arthur Flageul <arthur.flageul@gmail.com>
|
||||
Arthur Flageul <arthur.flageul@gmail.com> <arthur.flageul@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>
|
||||
Austin Vazquez <austin.vazquez@docker.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com> <55906459+austinvazquez@users.noreply.github.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com> <austin.vazquez.dev@gmail.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com> <macedonv@amazon.com>
|
||||
Avi Miller <avi.miller@oracle.com> <avi.miller@gmail.com>
|
||||
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>
|
||||
@ -85,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>
|
||||
@ -109,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>
|
||||
@ -131,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>
|
||||
@ -151,24 +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 Dooling <david.dooling@docker.com>
|
||||
David Dooling <david.dooling@docker.com> <dooling@gmail.com>
|
||||
David Karlsson <david.karlsson@docker.com>
|
||||
David Karlsson <david.karlsson@docker.com> <35727626+dvdksn@users.noreply.github.com>
|
||||
David M. Karr <davidmichaelkarr@gmail.com>
|
||||
David 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>
|
||||
@ -197,20 +153,14 @@ 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>
|
||||
George Kontridze <george@bugsnag.com>
|
||||
Gerwim Feiken <g.feiken@tfe.nl> <gerwim@gmail.com>
|
||||
Giau. Tran Minh <hello@giautm.dev>
|
||||
Giau. Tran Minh <hello@giautm.dev> <12751435+giautm@users.noreply.github.com>
|
||||
Giampaolo Mancini <giampaolo@trampolineup.com>
|
||||
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>
|
||||
@ -218,14 +168,12 @@ Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume@charmes.net>
|
||||
Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume@docker.com>
|
||||
Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume@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>
|
||||
Hakan Özler <hakan.ozler@kodcu.com>
|
||||
Hao Shu Wei <haosw@cn.ibm.com>
|
||||
Hao Shu Wei <haosw@cn.ibm.com> <haoshuwei1989@163.com>
|
||||
Harald Albers <github@albersweb.de>
|
||||
Harald Albers <github@albersweb.de> <albers@users.noreply.github.com>
|
||||
Harold Cooper <hrldcpr@gmail.com>
|
||||
Harry Zhang <harryz@hyper.sh> <harryzhang@zju.edu.cn>
|
||||
@ -252,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>
|
||||
@ -270,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>
|
||||
@ -280,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>
|
||||
@ -318,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>
|
||||
@ -337,13 +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>
|
||||
Li Yi <denverdino@gmail.com>
|
||||
Li Yi <denverdino@gmail.com> <weiyuan.yl@alibaba-inc.com>
|
||||
Liang Mingqiang <mqliang.zju@gmail.com>
|
||||
Liang-Chi Hsieh <viirya@gmail.com>
|
||||
Liao Qingwei <liaoqingwei@huawei.com>
|
||||
@ -353,7 +289,6 @@ Lokesh Mandvekar <lsm5@fedoraproject.org> <lsm5@redhat.com>
|
||||
Lorenzo Fontana <lo@linux.com> <fontanalorenzo@me.com>
|
||||
Louis Opter <kalessin@kalessin.fr>
|
||||
Louis Opter <kalessin@kalessin.fr> <louis@dotcloud.com>
|
||||
Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
|
||||
Luca Favatella <luca.favatella@erlang-solutions.com> <lucafavatella@users.noreply.github.com>
|
||||
Luke Marsden <me@lukemarsden.net> <luke@digital-crocus.com>
|
||||
Lyn <energylyn@zju.edu.cn>
|
||||
@ -369,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>
|
||||
@ -392,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>
|
||||
@ -406,12 +338,9 @@ 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>
|
||||
Mohammad Hossein <mhm98035@gmail.com>
|
||||
Mohit Soni <mosoni@ebay.com> <mohitsoni1989@gmail.com>
|
||||
Moorthy RS <rsmoorthy@gmail.com> <rsmoorthy@users.noreply.github.com>
|
||||
Morten Hekkvang <morten.hekkvang@sbab.se>
|
||||
@ -423,61 +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 St. laurent <patrick@saint-laurent.us>
|
||||
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>
|
||||
@ -499,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>
|
||||
@ -515,7 +425,6 @@ Stephen Day <stevvooe@gmail.com> <stephen.day@docker.com>
|
||||
Stephen Day <stevvooe@gmail.com> <stevvooe@users.noreply.github.com>
|
||||
Steve Desmond <steve@vtsv.ca> <stevedesmond-ca@users.noreply.github.com>
|
||||
Steve Richards <steve.richards@docker.com> stevejr <>
|
||||
Stuart Williams <pid@pidster.com>
|
||||
Sun Gengze <690388648@qq.com>
|
||||
Sun Jianbo <wonderflow.sun@gmail.com>
|
||||
Sun Jianbo <wonderflow.sun@gmail.com> <wonderflow@zju.edu.cn>
|
||||
@ -530,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>
|
||||
@ -550,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>
|
||||
@ -559,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>
|
||||
@ -627,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>
|
||||
|
||||
206
AUTHORS
206
AUTHORS
@ -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,32 +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>
|
||||
Allie Sadler <allie.sadler@docker.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>
|
||||
@ -60,15 +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 He <he.andrew.mail@gmail.com>
|
||||
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>
|
||||
@ -83,40 +67,27 @@ Antonis Kalipetis <akalipetis@gmail.com>
|
||||
Anusha Ragunathan <anusha.ragunathan@docker.com>
|
||||
Ao Li <la9249@163.com>
|
||||
Arash Deshmeh <adeshmeh@ca.ibm.com>
|
||||
Archimedes Trajano <developer@trajano.net>
|
||||
Arko Dasgupta <arko@tetrate.io>
|
||||
Arnaud Porterie <icecrime@gmail.com>
|
||||
Arnaud Rebillout <elboulangero@gmail.com>
|
||||
Arthur Flageul <arthur.flageul@gmail.com>
|
||||
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>
|
||||
Austin Vazquez <austin.vazquez@docker.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>
|
||||
@ -124,25 +95,18 @@ 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>
|
||||
carsontham <carsontham@outlook.com>
|
||||
Carston Schilds <Carston.Schilds@visier.com>
|
||||
Casey Korver <casey@korver.dev>
|
||||
Ce Gao <ce.gao@outlook.com>
|
||||
Cedric Davies <cedricda@microsoft.com>
|
||||
Cesar Talledo <cesar.talledo@docker.com>
|
||||
Cezar Sa Espinola <cezarsa@gmail.com>
|
||||
Chad Faragher <wyckster@hotmail.com>
|
||||
Chao Wang <wangchao.fnst@cn.fujitsu.com>
|
||||
@ -150,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>
|
||||
@ -172,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>
|
||||
@ -183,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>
|
||||
@ -197,8 +149,6 @@ Daisuke Ito <itodaisuke00@gmail.com>
|
||||
dalanlan <dalanlan925@gmail.com>
|
||||
Damien Nadé <github@livna.org>
|
||||
Dan Cotora <dan@bluevision.ro>
|
||||
Dan Wallis <dan@wallis.nz>
|
||||
Danial Gharib <danial.mail.gh@gmail.com>
|
||||
Daniel Artine <daniel.artine@ufrj.br>
|
||||
Daniel Cassidy <mail@danielcassidy.me.uk>
|
||||
Daniel Dao <dqminh@cloudflare.com>
|
||||
@ -220,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 <david.dooling@docker.com>
|
||||
David Dooling <dooling@gmail.com>
|
||||
David Gageot <david@gageot.net>
|
||||
David Karlsson <david.karlsson@docker.com>
|
||||
David le Blanc <systemmonkey42@users.noreply.github.com>
|
||||
David Lechner <david@lechnology.com>
|
||||
David Scott <dave@recoil.org>
|
||||
David Sheets <dsheets@docker.com>
|
||||
@ -239,47 +186,37 @@ 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>
|
||||
Dieter Reuter <dieter.reuter@me.com>
|
||||
Dilep Dev <34891655+DilepDev@users.noreply.github.com>
|
||||
Dima Stopel <dima@twistlock.com>
|
||||
Dimitry Andric <d.andric@activevideo.com>
|
||||
Ding Fei <dingfei@stars.org.cn>
|
||||
Diogo Monica <diogo@docker.com>
|
||||
Djordje Lukic <djordje.lukic@docker.com>
|
||||
Dmitriy Fishman <fishman.code@gmail.com>
|
||||
Dmitry Gusev <dmitry.gusev@gmail.com>
|
||||
Dmitry Smirnov <onlyjob@member.fsf.org>
|
||||
Dmitry V. Krivenok <krivenok.dmitry@gmail.com>
|
||||
Dominik Braun <dominik.braun@nbsp.de>
|
||||
Don Kjer <don.kjer@gmail.com>
|
||||
Dong Chen <dongluo.chen@docker.com>
|
||||
DongGeon Lee <secmatth1996@gmail.com>
|
||||
Doug Davis <dug@us.ibm.com>
|
||||
Drew Erny <derny@mirantis.com>
|
||||
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>
|
||||
Eng Zer Jun <engzerjun@gmail.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>
|
||||
@ -288,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>
|
||||
@ -308,34 +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>
|
||||
Giau. Tran Minh <hello@giautm.dev>
|
||||
Giedrius Jonikas <giedriusj1@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>
|
||||
@ -350,16 +272,12 @@ Henning Sprang <henning.sprang@gmail.com>
|
||||
Henry N <henrynmail-github@yahoo.de>
|
||||
Hernan Garcia <hernandanielg@gmail.com>
|
||||
Hongbin Lu <hongbin034@gmail.com>
|
||||
Hossein Abbasi <16090309+hsnabszhdn@users.noreply.github.com>
|
||||
Hu Keping <hukeping@huawei.com>
|
||||
Huayi Zhang <irachex@gmail.com>
|
||||
Hugo Chastel <Hugo-C@users.noreply.github.com>
|
||||
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 MacDonald <IJMacD@gmail.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>
|
||||
@ -369,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>
|
||||
@ -388,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>
|
||||
@ -408,12 +319,9 @@ Jesse Adametz <jesseadametz@gmail.com>
|
||||
Jessica Frazelle <jess@oxide.computer>
|
||||
Jezeniel Zapanta <jpzapanta22@gmail.com>
|
||||
Jian Zhang <zhangjian.fnst@cn.fujitsu.com>
|
||||
Jianyong Wu <wujianyong@hygon.cn>
|
||||
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>
|
||||
@ -430,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>
|
||||
@ -440,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>
|
||||
@ -458,13 +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>
|
||||
Julio Cesar Garcia <juliogarciamelgarejo@gmail.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>
|
||||
@ -483,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>
|
||||
@ -498,32 +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>
|
||||
Lajos Papp <lajos.papp@sequenceiq.com>
|
||||
Lars Kellogg-Stedman <lars@redhat.com>
|
||||
Laura Brehm <laurabrehm@hey.com>
|
||||
Laura Frank <ljfrank@gmail.com>
|
||||
Laurent Erignoux <lerignoux@gmail.com>
|
||||
Laurent Goderre <laurent.goderre@docker.com>
|
||||
Lee Gaines <eightlimbed@gmail.com>
|
||||
Lei Jitang <leijitang@huawei.com>
|
||||
Lennie <github@consolejunkie.net>
|
||||
lentil32 <lentil32@icloud.com>
|
||||
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 Zeghong <zeghong@hotmail.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>
|
||||
@ -535,11 +426,9 @@ lixiaobing10051267 <li.xiaobing1@zte.com.cn>
|
||||
Lloyd Dewolf <foolswisdom@gmail.com>
|
||||
Lorenzo Fontana <lo@linux.com>
|
||||
Louis Opter <kalessin@kalessin.fr>
|
||||
Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
|
||||
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>
|
||||
@ -556,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>
|
||||
@ -572,28 +458,20 @@ 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>
|
||||
Matthew Heon <mheon@redhat.com>
|
||||
Matthieu Hauglustaine <matt.hauglustaine@gmail.com>
|
||||
Matthieu MOREL <matthieu.morel35@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>
|
||||
Mert Şişmanoğlu <mert190737fb@gmail.com>
|
||||
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>
|
||||
@ -601,7 +479,6 @@ Michael Prokop <github@michael-prokop.at>
|
||||
Michael Scharf <github@scharf.gr>
|
||||
Michael Spetsiotis <michael_spets@hotmail.com>
|
||||
Michael Steinert <mike.steinert@gmail.com>
|
||||
Michael Tews <michael@tews.dev>
|
||||
Michael West <mwest@mdsol.com>
|
||||
Michal Minář <miminar@redhat.com>
|
||||
Michał Czeraszkiewicz <czerasz@gmail.com>
|
||||
@ -610,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>
|
||||
@ -622,19 +498,14 @@ Mindaugas Rukas <momomg@gmail.com>
|
||||
Miroslav Gula <miroslav.gula@naytrolabs.com>
|
||||
Misty Stanley-Jones <misty@docker.com>
|
||||
Mohammad Banikazemi <mb@us.ibm.com>
|
||||
Mohammad Hossein <mhm98035@gmail.com>
|
||||
Mohammed Aaqib Ansari <maaquib@gmail.com>
|
||||
Mohammed Aminu Futa <mohammedfuta2000@gmail.com>
|
||||
Mohini Anne Dsouza <mohini3917@gmail.com>
|
||||
Moorthy RS <rsmoorthy@gmail.com>
|
||||
Morgan Bauer <mbauer@us.ibm.com>
|
||||
Morten Hekkvang <morten.hekkvang@sbab.se>
|
||||
Morten Linderud <morten@linderud.pw>
|
||||
Moysés Borges <moysesb@gmail.com>
|
||||
Mozi <29089388+pzhlkj6612@users.noreply.github.com>
|
||||
Mrunal Patel <mrunalp@gmail.com>
|
||||
muicoder <muicoder@gmail.com>
|
||||
Murukesh Mohanan <murukesh.mohanan@gmail.com>
|
||||
Muthukumar R <muthur@gmail.com>
|
||||
Máximo Cuadros <mcuadros@gmail.com>
|
||||
Mårten Cassel <marten.cassel@gmail.com>
|
||||
@ -650,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>
|
||||
@ -659,18 +528,13 @@ Nicolas De Loof <nicolas.deloof@gmail.com>
|
||||
Nikhil Chawla <chawlanikhil24@gmail.com>
|
||||
Nikolas Garofil <nikolas.garofil@uantwerpen.be>
|
||||
Nikolay Milovanov <nmil@itransformers.net>
|
||||
NinaLua <iturf@sina.cn>
|
||||
Nir Soffer <nsoffer@redhat.com>
|
||||
Nishant Totla <nishanttotla@gmail.com>
|
||||
NIWA Hideyuki <niwa.niwa@nifty.ne.jp>
|
||||
Noah Silas <noah@hustle.com>
|
||||
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>
|
||||
@ -678,24 +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>
|
||||
Patrick St. laurent <patrick@saint-laurent.us>
|
||||
Paul <paul9869@gmail.com>
|
||||
Paul Kehrer <paul.l.kehrer@gmail.com>
|
||||
Paul Lietar <paul@lietar.net>
|
||||
Paul Mulders <justinkb@gmail.com>
|
||||
Paul Rogalski <mail@paul-rogalski.de>
|
||||
Paul Seyfert <pseyfert.mathphys@gmail.com>
|
||||
Paul Weaver <pauweave@cisco.com>
|
||||
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>
|
||||
@ -703,12 +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>
|
||||
Pieter E Smit <diepes@github.com>
|
||||
pidster <pid@pidster.com>
|
||||
pixelistik <pixelistik@users.noreply.github.com>
|
||||
Pratik Karki <prertik@outlook.com>
|
||||
Prayag Verma <prayag.verma@gmail.com>
|
||||
@ -716,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>
|
||||
@ -728,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>
|
||||
@ -749,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>
|
||||
@ -759,18 +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>
|
||||
Sarah Sanders <sarah.sanders@docker.com>
|
||||
Sargun Dhillon <sargun@netflix.com>
|
||||
Saswat Bhattacharya <sas.saswat@gmail.com>
|
||||
Saurabh Kumar <saurabhkumar0184@gmail.com>
|
||||
Scott Brenner <scott@scottbrenner.me>
|
||||
Scott Collier <emailscottcollier@gmail.com>
|
||||
Sean Christopherson <sean.j.christopherson@intel.com>
|
||||
@ -797,10 +643,8 @@ 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>
|
||||
Stavros Panakakis <stavrospanakakis@gmail.com>
|
||||
Stefan S. <tronicum@user.github.com>
|
||||
Stefan Scherer <stefan.scherer@docker.com>
|
||||
Stefan Weil <sw@weilnetz.de>
|
||||
@ -810,8 +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>
|
||||
Stuart Williams <pid@pidster.com>
|
||||
Subhajit Ghosh <isubuz.g@gmail.com>
|
||||
Sun Jianbo <wonderflow.sun@gmail.com>
|
||||
Sune Keller <absukl@almbrand.dk>
|
||||
@ -823,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>
|
||||
@ -846,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>
|
||||
@ -858,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>
|
||||
@ -873,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>
|
||||
@ -899,11 +733,9 @@ Wang Yumu <37442693@qq.com>
|
||||
Wataru Ishida <ishida.wataru@lab.ntt.co.jp>
|
||||
Wayne Song <wsong@docker.com>
|
||||
Wen Cheng Ma <wenchma@cn.ibm.com>
|
||||
Wenlong Zhang <zhangwenlong@loongson.cn>
|
||||
Wenzhi Liang <wenzhi.liang@gmail.com>
|
||||
Wes Morgan <cap10morgan@gmail.com>
|
||||
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
|
||||
Will Wang <willww64@gmail.com>
|
||||
William Henry <whenry@redhat.com>
|
||||
Xianglin Gao <xlgao@zju.edu.cn>
|
||||
Xiaodong Liu <liuxiaodong@loongson.cn>
|
||||
@ -920,26 +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>
|
||||
林博仁 Buo-ren Lin <Buo.Ren.Lin@gmail.com>
|
||||
|
||||
@ -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.
|
||||
|
||||
128
Dockerfile
128
Dockerfile
@ -1,43 +1,14 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG BASE_VARIANT=alpine
|
||||
|
||||
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
|
||||
# It must be a supported tag in the docker.io/library/alpine image repository
|
||||
# that's also available as alpine image variant for the Golang version used.
|
||||
ARG ALPINE_VERSION=3.22
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.25.5
|
||||
|
||||
# XX_VERSION specifies the version of the xx utility to use.
|
||||
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
|
||||
ARG XX_VERSION=1.7.0
|
||||
|
||||
# GOVERSIONINFO_VERSION is the version of GoVersionInfo to install.
|
||||
# It must be a valid tag from https://github.com/josephspurrier/goversioninfo
|
||||
ARG GOVERSIONINFO_VERSION=v1.5.0
|
||||
|
||||
# GOTESTSUM_VERSION sets the version of gotestsum to install in the dev container.
|
||||
# It must be a valid tag in the https://github.com/gotestyourself/gotestsum repository.
|
||||
ARG GOTESTSUM_VERSION=v1.13.0
|
||||
|
||||
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
|
||||
# It must be a tag in the docker.io/docker/buildx-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG BUILDX_VERSION=0.29.1
|
||||
|
||||
# COMPOSE_VERSION is the version of compose to install in the dev container.
|
||||
# It must be a tag in the docker.io/docker/compose-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG COMPOSE_VERSION=v2.40.0
|
||||
ARG GO_VERSION=1.18.7
|
||||
ARG XX_VERSION=1.1.0
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-base-alpine
|
||||
ENV GOTOOLCHAIN=local
|
||||
COPY --link --from=xx / /
|
||||
RUN apk add --no-cache bash clang lld llvm file git git-daemon
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-${BASE_VARIANT} AS build-base-alpine
|
||||
COPY --from=xx / /
|
||||
RUN apk add --no-cache clang lld llvm file git
|
||||
WORKDIR /go/src/github.com/docker/cli
|
||||
|
||||
FROM build-base-alpine AS build-alpine
|
||||
@ -45,28 +16,14 @@ 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
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-buster AS build-base-buster
|
||||
COPY --from=xx / /
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y clang lld file
|
||||
WORKDIR /go/src/github.com/docker/cli
|
||||
|
||||
FROM build-base-debian AS build-debian
|
||||
FROM build-base-buster AS build-buster
|
||||
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
|
||||
RUN xx-apt install --no-install-recommends -y libc6-dev libgcc-8-dev
|
||||
|
||||
FROM build-${BASE_VARIANT} AS build
|
||||
# GO_LINKMODE defines if static or dynamic binary should be produced
|
||||
@ -79,73 +36,16 @@ ARG GO_STRIP
|
||||
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
|
||||
RUN --mount=ro --mount=type=cache,target=/root/.cache \
|
||||
--mount=from=dockercore/golang-cross:xx-sdk-extras,target=/xx-sdk,src=/xx-sdk \
|
||||
--mount=type=tmpfs,target=cli/winresources \
|
||||
xx-go --wrap && \
|
||||
# export GOCACHE=$(go env GOCACHE)/$(xx-info)$([ -f /etc/alpine-release ] && echo "alpine") && \
|
||||
TARGET=/out ./scripts/build/binary && \
|
||||
xx-verify $([ "$GO_LINKMODE" = "static" ] && echo "--static") /out/docker
|
||||
|
||||
FROM build-${BASE_VARIANT} AS test
|
||||
COPY --link --from=gotestsum /out/gotestsum /usr/bin/gotestsum
|
||||
ENV GO111MODULE=auto
|
||||
RUN --mount=type=bind,target=.,rw \
|
||||
--mount=type=cache,target=/root/.cache \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
gotestsum -- -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust')
|
||||
|
||||
FROM scratch AS test-coverage
|
||||
COPY --from=test /tmp/coverage.txt /coverage.txt
|
||||
|
||||
FROM build-${BASE_VARIANT} AS build-plugins
|
||||
ARG GO_LINKMODE=static
|
||||
ARG GO_BUILDTAGS
|
||||
ARG GO_STRIP
|
||||
ARG CGO_ENABLED
|
||||
ARG VERSION
|
||||
RUN --mount=ro --mount=type=cache,target=/root/.cache \
|
||||
xx-go --wrap && \
|
||||
TARGET=/out ./scripts/build/plugins e2e/cli-plugins/plugins/*
|
||||
|
||||
FROM build-base-alpine AS e2e-base-alpine
|
||||
RUN apk add --no-cache build-base curl openssl openssh-client
|
||||
|
||||
FROM build-base-debian AS e2e-base-debian
|
||||
RUN apt-get update && apt-get install -y build-essential curl openssl openssh-client
|
||||
|
||||
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
|
||||
FROM docker/compose-bin:${COMPOSE_VERSION} AS compose
|
||||
|
||||
FROM e2e-base-${BASE_VARIANT} AS e2e
|
||||
COPY --link --from=gotestsum /out/gotestsum /usr/bin/gotestsum
|
||||
COPY --link --from=build /out ./build/
|
||||
COPY --link --from=build-plugins /out ./build/
|
||||
COPY --link --from=buildx /buildx /usr/libexec/docker/cli-plugins/docker-buildx
|
||||
COPY --link --from=compose /docker-compose /usr/libexec/docker/cli-plugins/docker-compose
|
||||
COPY --link . .
|
||||
ENV DOCKER_BUILDKIT=1
|
||||
ENV PATH=/go/src/github.com/docker/cli/build:$PATH
|
||||
CMD ["./scripts/test/e2e/entry"]
|
||||
|
||||
FROM build-base-${BASE_VARIANT} AS dev
|
||||
COPY --link . .
|
||||
|
||||
FROM scratch AS plugins
|
||||
COPY --from=build-plugins /out .
|
||||
|
||||
FROM scratch AS bin-image-linux
|
||||
COPY --from=build /out/docker /docker
|
||||
FROM scratch AS bin-image-darwin
|
||||
COPY --from=build /out/docker /docker
|
||||
FROM scratch AS bin-image-windows
|
||||
COPY --from=build /out/docker /docker.exe
|
||||
|
||||
FROM bin-image-${TARGETOS} AS bin-image
|
||||
COPY . .
|
||||
|
||||
FROM scratch AS binary
|
||||
COPY --from=build /out .
|
||||
|
||||
47
Jenkinsfile
vendored
Normal file
47
Jenkinsfile
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
pipeline {
|
||||
agent {
|
||||
label "amd64 && ubuntu-1804 && overlay2"
|
||||
}
|
||||
|
||||
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) - 19.03 engine") {
|
||||
steps {
|
||||
sh "E2E_ENGINE_VERSION=19.03-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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
MAINTAINERS
31
MAINTAINERS
@ -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"
|
||||
|
||||
|
||||
151
Makefile
151
Makefile
@ -1,139 +1,94 @@
|
||||
#
|
||||
# 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/|/cmd/docker-trust')} $(TESTFLAGS)
|
||||
|
||||
.PHONY: test-coverage
|
||||
test-coverage: ## run test coverage
|
||||
mkdir -p $(CURDIR)/build/coverage
|
||||
gotestsum -- $(shell go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust') -coverprofile=$(CURDIR)/build/coverage/coverage.txt
|
||||
gotestsum -- -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.24 . ; \
|
||||
else \
|
||||
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \
|
||||
fi
|
||||
gometalinter --config gometalinter.json ./...
|
||||
|
||||
.PHONY: binary
|
||||
binary: ## build executable for Linux
|
||||
binary:
|
||||
./scripts/build/binary
|
||||
|
||||
.PHONY: plugins
|
||||
plugins: ## build example CLI plugins
|
||||
./scripts/build/plugins
|
||||
|
||||
.PHONY: cross
|
||||
cross:
|
||||
./scripts/build/binary
|
||||
|
||||
.PHONY: plugins-windows
|
||||
plugins-windows: ## build example CLI plugins for Windows
|
||||
./scripts/build/plugins-windows
|
||||
|
||||
.PHONY: plugins-osx
|
||||
plugins-osx: ## build example CLI plugins for macOS
|
||||
./scripts/build/plugins-osx
|
||||
|
||||
.PHONY: dynbinary
|
||||
dynbinary: ## build dynamically linked binary
|
||||
GO_LINKMODE=dynamic ./scripts/build/binary
|
||||
|
||||
.PHONY: plugins
|
||||
plugins: ## build example CLI plugins
|
||||
scripts/build/plugins
|
||||
|
||||
.PHONY: trust-plugin
|
||||
trust-plugin: ## build docker-trust CLI plugins
|
||||
scripts/build/trust-plugin
|
||||
|
||||
.PHONY: install-trust-plugin
|
||||
install-trust-plugin: trust-plugin
|
||||
install-trust-plugin: ## install docker-trust CLI plugins
|
||||
install -D -m 0755 "$$(readlink -f build/docker-trust)" /usr/libexec/docker/cli-plugins/docker-trust
|
||||
|
||||
.PHONY: vendor
|
||||
vendor: ## update vendor with go modules
|
||||
vendor: vendor.conf ## check that vendor matches vendor.conf
|
||||
rm -rf vendor
|
||||
scripts/with-go-mod.sh scripts/vendor update
|
||||
|
||||
.PHONY: validate-vendor
|
||||
validate-vendor: ## validate vendor
|
||||
scripts/with-go-mod.sh scripts/vendor validate
|
||||
|
||||
.PHONY: mod-outdated
|
||||
mod-outdated: ## check outdated dependencies
|
||||
scripts/with-go-mod.sh scripts/vendor outdated
|
||||
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/with-go-mod.sh scripts/docs/generate-man.sh
|
||||
|
||||
.PHONY: mddocs
|
||||
mddocs: ## generate markdown files from go source
|
||||
scripts/with-go-mod.sh scripts/docs/generate-md.sh
|
||||
scripts/docs/generate-man.sh
|
||||
|
||||
.PHONY: yamldocs
|
||||
yamldocs: ## generate documentation YAML files consumed by docs repo
|
||||
scripts/with-go-mod.sh scripts/docs/generate-yaml.sh
|
||||
scripts/docs/generate-yaml.sh
|
||||
|
||||
.PHONY: shellcheck
|
||||
shellcheck: ## run shellcheck validation
|
||||
scripts/validate/shellcheck
|
||||
|
||||
.PHONY: help
|
||||
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
2
NOTICE
@ -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.
|
||||
|
||||
64
README.md
64
README.md
@ -1,74 +1,66 @@
|
||||
# Docker CLI
|
||||
[](https://circleci.com/gh/docker/cli/tree/master)
|
||||
[](https://ci.docker.com/public/job/cli/job/master)
|
||||
|
||||
[](https://pkg.go.dev/github.com/docker/cli)
|
||||
[](https://github.com/docker/cli/actions?query=workflow%3Abuild)
|
||||
[](https://github.com/docker/cli/actions?query=workflow%3Atest)
|
||||
[](https://goreportcard.com/report/github.com/docker/cli)
|
||||
[](https://scorecard.dev/viewer/?uri=github.com/docker/cli)
|
||||
[](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:
|
||||
|
||||
```shell
|
||||
docker buildx bake
|
||||
```
|
||||
$ docker buildx bake
|
||||
```
|
||||
|
||||
Build binaries for all supported platforms:
|
||||
|
||||
```shell
|
||||
docker buildx bake cross
|
||||
```
|
||||
$ docker buildx bake cross
|
||||
```
|
||||
|
||||
Build for a specific platform:
|
||||
|
||||
```shell
|
||||
docker buildx bake --set binary.platform=linux/arm64
|
||||
```
|
||||
$ docker buildx bake --set binary.platform=linux/arm64
|
||||
```
|
||||
|
||||
Build dynamic binary for glibc or musl:
|
||||
|
||||
```shell
|
||||
USE_GLIBC=1 docker buildx bake dynbinary
|
||||
```
|
||||
$ USE_GLIBC=1 docker buildx bake dynbinary
|
||||
```
|
||||
|
||||
|
||||
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
|
||||
|
||||
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 +68,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.
|
||||
|
||||
44
SECURITY.md
44
SECURITY.md
@ -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.
|
||||
@ -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).
|
||||
|
||||
23
appveyor.yml
Normal file
23
appveyor.yml
Normal file
@ -0,0 +1,23 @@
|
||||
version: "{build}"
|
||||
|
||||
clone_folder: c:\gopath\src\github.com\docker\cli
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
GOVERSION: 1.18.7
|
||||
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
|
||||
@ -5,32 +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/moby/moby/client"
|
||||
"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(), client.PingOptions{})
|
||||
cli := dockerCli.Client()
|
||||
ping, err := cli.Ping(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Println(ping.APIVersion)
|
||||
fmt.Println(ping.APIVersion)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@ -39,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",
|
||||
@ -57,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()
|
||||
},
|
||||
}
|
||||
|
||||
@ -93,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",
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,15 @@
|
||||
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
|
||||
type Candidate interface {
|
||||
Path() string
|
||||
Metadata() ([]byte, error)
|
||||
}
|
||||
|
||||
type candidate struct {
|
||||
path string
|
||||
}
|
||||
@ -15,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()
|
||||
}
|
||||
|
||||
@ -6,10 +6,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
type fakeCandidate struct {
|
||||
@ -31,132 +30,61 @@ func (c *fakeCandidate) Metadata() ([]byte, error) {
|
||||
|
||||
func TestValidateCandidate(t *testing.T) {
|
||||
const (
|
||||
goodPluginName = metadata.NamePrefix + "goodplugin"
|
||||
builtinName = metadata.NamePrefix + "builtin"
|
||||
builtinAlias = metadata.NamePrefix + "alias"
|
||||
goodPluginName = NamePrefix + "goodplugin"
|
||||
|
||||
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
|
||||
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
|
||||
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
|
||||
builtinName = NamePrefix + "builtin"
|
||||
builtinAlias = NamePrefix + "alias"
|
||||
|
||||
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
|
||||
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
|
||||
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
|
||||
metaExperimental = `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`
|
||||
)
|
||||
|
||||
fakeroot := &cobra.Command{Use: "docker"}
|
||||
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),
|
||||
},
|
||||
})
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
plugin *fakeCandidate
|
||||
name string
|
||||
c *fakeCandidate
|
||||
|
||||
// Either err or invalid may be non-empty, but not both (both can be empty for a good plugin).
|
||||
err string
|
||||
invalid string
|
||||
expVer string
|
||||
}{
|
||||
// Invalid cases.
|
||||
{
|
||||
name: "empty path",
|
||||
plugin: &fakeCandidate{path: ""},
|
||||
err: "plugin candidate path cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "bad prefix",
|
||||
plugin: &fakeCandidate{path: badPrefixPath},
|
||||
err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix),
|
||||
},
|
||||
{
|
||||
name: "bad path",
|
||||
plugin: &fakeCandidate{path: badNamePath},
|
||||
invalid: "did not match",
|
||||
},
|
||||
{
|
||||
name: "builtin command",
|
||||
plugin: &fakeCandidate{path: builtinName},
|
||||
invalid: `plugin "builtin" duplicates builtin command`,
|
||||
},
|
||||
{
|
||||
name: "builtin alias",
|
||||
plugin: &fakeCandidate{path: builtinAlias},
|
||||
invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`,
|
||||
},
|
||||
{
|
||||
name: "fetch failure",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: false},
|
||||
invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath),
|
||||
},
|
||||
{
|
||||
name: "metadata not json",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`},
|
||||
invalid: "invalid character",
|
||||
},
|
||||
{
|
||||
name: "empty schemaversion",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`},
|
||||
invalid: `plugin SchemaVersion version cannot be empty`,
|
||||
},
|
||||
{
|
||||
name: "invalid schemaversion",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`},
|
||||
invalid: `plugin SchemaVersion "xyzzy" has wrong format: must be <major>.<minor>.<patch>`,
|
||||
},
|
||||
{
|
||||
name: "invalid schemaversion major",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "2.0.0"}`},
|
||||
invalid: `plugin SchemaVersion "2.0.0" is not supported: must be lower than 2.0.0`,
|
||||
},
|
||||
{
|
||||
name: "no vendor",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`},
|
||||
invalid: "plugin metadata does not define a vendor",
|
||||
},
|
||||
{
|
||||
name: "empty vendor",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`},
|
||||
invalid: "plugin metadata does not define a vendor",
|
||||
},
|
||||
|
||||
// Valid cases.
|
||||
{
|
||||
name: "valid",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`},
|
||||
expVer: "0.1.0",
|
||||
},
|
||||
{
|
||||
// Including the deprecated "experimental" field should not break processing.
|
||||
name: "with legacy experimental",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`},
|
||||
expVer: "0.1.0",
|
||||
},
|
||||
{
|
||||
// note that this may not be supported by older CLIs
|
||||
name: "new minor schema version",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.2.0", "Vendor": "e2e-testing"}`},
|
||||
expVer: "0.2.0",
|
||||
},
|
||||
{
|
||||
// note that this may not be supported by older CLIs
|
||||
name: "new major schema version",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "1.0.0", "Vendor": "e2e-testing"}`},
|
||||
expVer: "1.0.0",
|
||||
},
|
||||
/* Each failing one of the tests */
|
||||
{name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
|
||||
{name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", NamePrefix)},
|
||||
{name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
|
||||
{name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`},
|
||||
{name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`},
|
||||
{name: "fetch failure", c: &fakeCandidate{path: goodPluginPath, exec: false}, invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath)},
|
||||
{name: "metadata not json", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`}, invalid: "invalid character"},
|
||||
{name: "empty schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`}, invalid: `plugin SchemaVersion "" is not valid`},
|
||||
{name: "invalid schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`}, invalid: `plugin SchemaVersion "xyzzy" is not valid`},
|
||||
{name: "no vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`}, invalid: "plugin metadata does not define a vendor"},
|
||||
{name: "empty vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`}, invalid: "plugin metadata does not define a vendor"},
|
||||
// This one should work
|
||||
{name: "valid", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}},
|
||||
{name: "experimental + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p, err := newPlugin(tc.plugin, fakeroot.Commands())
|
||||
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, is.ErrorType(p.Err, reflect.TypeOf(&pluginError{})))
|
||||
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, p.SchemaVersion, tc.expVer)
|
||||
assert.Equal(t, NamePrefix+p.Name, goodPluginName)
|
||||
assert.Equal(t, p.SchemaVersion, "0.1.0")
|
||||
assert.Equal(t, p.Vendor, "e2e-testing")
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,78 +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,
|
||||
Hidden: p.Hidden,
|
||||
Run: func(_ *cobra.Command, _ []string) {},
|
||||
Annotations: annotations,
|
||||
DisableFlagParsing: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
flags := rootCmd.PersistentFlags()
|
||||
flags.SetOutput(nil)
|
||||
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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -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.24
|
||||
|
||||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// pluginError is set as Plugin.Err by NewPlugin if the plugin
|
||||
@ -23,6 +20,11 @@ func (e *pluginError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
// Cause satisfies the errors.causer interface for pluginError.
|
||||
func (e *pluginError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
// Unwrap provides compatibility for Go 1.13 error chains.
|
||||
func (e *pluginError) Unwrap() error {
|
||||
return e.cause
|
||||
@ -34,13 +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 {
|
||||
return &pluginError{cause: fmt.Errorf("%s: %w", msg, err)}
|
||||
return &pluginError{cause: errors.Wrap(err, msg)}
|
||||
}
|
||||
|
||||
// newPluginError creates a new pluginError, analogous to
|
||||
// NewPluginError creates a new pluginError, analogous to
|
||||
// errors.Errorf.
|
||||
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...)}
|
||||
}
|
||||
|
||||
@ -1,27 +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"))
|
||||
err := NewPluginError("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)))
|
||||
|
||||
err = wrapAsPluginError(nil, "wrapping")
|
||||
assert.Check(t, is.Error(err, "wrapping: %!w(<nil>)"))
|
||||
actual, err := yaml.Marshal(err)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, "'wrapping: testing'\n", string(actual))
|
||||
}
|
||||
|
||||
@ -1,200 +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 := getPluginDirs(cfg)
|
||||
nextSteps := make([]string, 0, len(pluginsCfg))
|
||||
for pluginName, pluginCfg := range pluginsCfg {
|
||||
match, ok := pluginMatch(pluginCfg, 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
|
||||
}
|
||||
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,167 +1,135 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
"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/docker/cli/cli/debug"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/errgroup"
|
||||
exec "golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
var pluginDirs []string
|
||||
type notFound interface{ NotFound() }
|
||||
|
||||
if cfg != nil {
|
||||
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
|
||||
// IsNotFound is true if the given error is due to a plugin not being found.
|
||||
func IsNotFound(err error) bool {
|
||||
if e, ok := err.(*pluginError); ok {
|
||||
err = e.Cause()
|
||||
}
|
||||
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
|
||||
pluginDirs = append(pluginDirs, pluginDir)
|
||||
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
|
||||
return pluginDirs
|
||||
_, ok := err.(notFound)
|
||||
return ok
|
||||
}
|
||||
|
||||
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 getPluginDirs(dockerCli command.Cli) ([]string, error) {
|
||||
var pluginDirs []string
|
||||
|
||||
if cfg := dockerCli.ConfigFile(); cfg != nil {
|
||||
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
|
||||
}
|
||||
pluginDir, err := config.Path("cli-plugins")
|
||||
if err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pluginDirs = append(pluginDirs, pluginDir)
|
||||
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
|
||||
return pluginDirs, nil
|
||||
}
|
||||
|
||||
func addPluginCandidatesFromDir(res map[string][]string, d string) error {
|
||||
dentries, err := ioutil.ReadDir(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, dentry := range dentries {
|
||||
switch mode := dentry.Type() & os.ModeType; mode { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list
|
||||
case os.ModeSymlink:
|
||||
if !debug.IsEnabled() {
|
||||
// Skip broken symlinks unless debug is enabled. With debug
|
||||
// enabled, this will print a warning in "docker info".
|
||||
if _, err := os.Stat(filepath.Join(d, dentry.Name())); errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
case 0:
|
||||
// Regular file, keep going
|
||||
switch dentry.Mode() & os.ModeType {
|
||||
case 0, os.ModeSymlink:
|
||||
// Regular file or symlink, keep going
|
||||
default:
|
||||
// Something else, ignore.
|
||||
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
|
||||
}
|
||||
|
||||
// GetPlugin returns a plugin on the system by its name
|
||||
func GetPlugin(name string, dockerCLI config.Provider, rootcmd *cobra.Command) (*Plugin, error) {
|
||||
pluginDirs := getPluginDirs(dockerCLI.ConfigFile())
|
||||
return getPlugin(name, pluginDirs, rootcmd)
|
||||
}
|
||||
|
||||
func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugin, error) {
|
||||
candidates := listPluginCandidates(pluginDirs)
|
||||
if paths, ok := candidates[name]; ok {
|
||||
if len(paths) == 0 {
|
||||
return nil, errPluginNotFound(name)
|
||||
}
|
||||
c := &candidate{paths[0]}
|
||||
p, err := newPlugin(c, rootcmd.Commands())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !errdefs.IsNotFound(p.Err) {
|
||||
p.ShadowedPaths = paths[1:]
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
return nil, errPluginNotFound(name)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListPlugins produces a list of the plugins available on the system
|
||||
func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, error) {
|
||||
pluginDirs := getPluginDirs(dockerCli.ConfigFile())
|
||||
candidates := listPluginCandidates(pluginDirs)
|
||||
if len(candidates) == 0 {
|
||||
return nil, nil
|
||||
func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
|
||||
pluginDirs, err := getPluginDirs(dockerCli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
candidates, err := listPluginCandidates(pluginDirs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var plugins []Plugin
|
||||
var mu sync.Mutex
|
||||
ctx := rootcmd.Context()
|
||||
if ctx == nil {
|
||||
// Fallback, mostly for tests that pass a bare cobra.command
|
||||
ctx = context.Background()
|
||||
}
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
cmds := rootcmd.Commands()
|
||||
for _, paths := range candidates {
|
||||
func(paths []string) {
|
||||
eg.Go(func() error {
|
||||
if len(paths) == 0 {
|
||||
return nil
|
||||
}
|
||||
c := &candidate{paths[0]}
|
||||
p, err := newPlugin(c, cmds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !errdefs.IsNotFound(p.Err) {
|
||||
p.ShadowedPaths = paths[1:]
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
plugins = append(plugins, p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}(paths)
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
if len(paths) == 0 {
|
||||
continue
|
||||
}
|
||||
c := &candidate{paths[0]}
|
||||
p, err := newPlugin(c, rootcmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !IsNotFound(p.Err) {
|
||||
p.ShadowedPaths = paths[1:]
|
||||
plugins = append(plugins, p)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(plugins, func(i, j int) bool {
|
||||
@ -171,21 +139,24 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e
|
||||
return plugins, nil
|
||||
}
|
||||
|
||||
// PluginRunCommand returns an [os/exec.Cmd] which when [os/exec.Cmd.Run] will execute the named plugin.
|
||||
// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
|
||||
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
|
||||
// The error returned satisfies the [errdefs.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) {
|
||||
// The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
|
||||
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.
|
||||
args := os.Args[1:]
|
||||
if !isValidPluginName(name) {
|
||||
if !pluginNameRe.MatchString(name) {
|
||||
// We treat this as "not found" so that callers will
|
||||
// fallback to their "invalid" command path.
|
||||
return nil, errPluginNotFound(name)
|
||||
}
|
||||
exename := addExeSuffix(metadata.NamePrefix + name)
|
||||
pluginDirs := getPluginDirs(dockerCli.ConfigFile())
|
||||
exename := addExeSuffix(NamePrefix + name)
|
||||
pluginDirs, err := getPluginDirs(dockerCli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, d := range pluginDirs {
|
||||
path := filepath.Join(d, exename)
|
||||
@ -199,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
|
||||
}
|
||||
@ -207,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
|
||||
@ -218,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"
|
||||
}
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
@ -38,7 +36,7 @@ func TestListPluginCandidates(t *testing.T) {
|
||||
"plugins3-target", // Will be referenced as a symlink from below
|
||||
fs.WithFile("docker-plugin1", ""),
|
||||
fs.WithDir("ignored3"),
|
||||
fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is ignored
|
||||
fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is still a candidate (but would fail tests later)
|
||||
fs.WithFile("non-plugin-symlinked", ""), // This shouldn't appear, but ...
|
||||
fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should.
|
||||
),
|
||||
@ -48,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"),
|
||||
@ -72,6 +71,9 @@ func TestListPluginCandidates(t *testing.T) {
|
||||
"hardlink2": {
|
||||
dir.Join("plugins2", "docker-hardlink2"),
|
||||
},
|
||||
"brokensymlink": {
|
||||
dir.Join("plugins3", "docker-brokensymlink"),
|
||||
},
|
||||
"symlinked": {
|
||||
dir.Join("plugins3", "docker-symlinked"),
|
||||
},
|
||||
@ -80,66 +82,14 @@ func TestListPluginCandidates(t *testing.T) {
|
||||
assert.DeepEqual(t, candidates, exp)
|
||||
}
|
||||
|
||||
func TestListPluginCandidatesEmpty(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
candidates := listPluginCandidates([]string{tmpDir, filepath.Join(tmpDir, "no-such-dir")})
|
||||
assert.Assert(t, len(candidates) == 0)
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/docker/cli/issues/5643.
|
||||
// Check that inaccessible directories that come before accessible ones are ignored
|
||||
// and do not prevent the latter from being processed.
|
||||
func 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, errdefs.IsNotFound(err))
|
||||
}
|
||||
|
||||
func TestListPluginsIsSorted(t *testing.T) {
|
||||
dir := fs.NewDir(t, t.Name(),
|
||||
fs.WithFile("docker-bbb", `
|
||||
#!/bin/sh
|
||||
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
|
||||
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()
|
||||
|
||||
@ -164,18 +114,21 @@ func TestErrPluginNotFound(t *testing.T) {
|
||||
var err error = errPluginNotFound("test")
|
||||
err.(errPluginNotFound).NotFound()
|
||||
assert.Error(t, err, "Error: No such CLI plugin: test")
|
||||
assert.Assert(t, errdefs.IsNotFound(err))
|
||||
assert.Assert(t, !errdefs.IsNotFound(nil))
|
||||
assert.Assert(t, IsNotFound(err))
|
||||
assert.Assert(t, !IsNotFound(nil))
|
||||
}
|
||||
|
||||
func TestGetPluginDirs(t *testing.T) {
|
||||
cli := test.NewFakeCli(nil)
|
||||
|
||||
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
|
||||
pluginDir, err := config.Path("cli-plugins")
|
||||
assert.NilError(t, err)
|
||||
expected := append([]string{pluginDir}, defaultSystemPluginDirs...)
|
||||
|
||||
pluginDirs := getPluginDirs(cli.ConfigFile())
|
||||
var pluginDirs []string
|
||||
pluginDirs, err = getPluginDirs(cli)
|
||||
assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))
|
||||
assert.NilError(t, err)
|
||||
|
||||
extras := []string{
|
||||
"foo", "bar", "baz",
|
||||
@ -184,6 +137,7 @@ func TestGetPluginDirs(t *testing.T) {
|
||||
cli.SetConfigFile(&configfile.ConfigFile{
|
||||
CLIPluginsExtraDirs: extras,
|
||||
})
|
||||
pluginDirs = getPluginDirs(cli.ConfigFile())
|
||||
pluginDirs, err = getPluginDirs(cli)
|
||||
assert.DeepEqual(t, expected, pluginDirs)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
@ -1,20 +1,9 @@
|
||||
//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",
|
||||
}
|
||||
|
||||
@ -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"),
|
||||
|
||||
28
cli-plugins/manager/metadata.go
Normal file
28
cli-plugins/manager/metadata.go
Normal file
@ -0,0 +1,28 @@
|
||||
package manager
|
||||
|
||||
const (
|
||||
// NamePrefix is the prefix required on all plugin binary names
|
||||
NamePrefix = "docker-"
|
||||
|
||||
// MetadataSubcommandName is the name of the plugin subcommand
|
||||
// which must be supported by every plugin and returns the
|
||||
// plugin metadata.
|
||||
MetadataSubcommandName = "docker-cli-plugin-metadata"
|
||||
)
|
||||
|
||||
// Metadata provided by the plugin.
|
||||
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"`
|
||||
}
|
||||
@ -1,24 +1,22 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"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]*$")
|
||||
)
|
||||
|
||||
// Plugin represents a potential plugin with all it's metadata.
|
||||
type Plugin struct {
|
||||
metadata.Metadata
|
||||
Metadata
|
||||
|
||||
Name string `json:",omitempty"`
|
||||
Path string `json:",omitempty"`
|
||||
@ -30,34 +28,14 @@ type Plugin struct {
|
||||
ShadowedPaths []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler] to handle marshaling the
|
||||
// [Plugin.Err] field (Go doesn't marshal errors by default).
|
||||
func (p *Plugin) MarshalJSON() ([]byte, error) {
|
||||
type Alias Plugin // avoid recursion
|
||||
|
||||
cp := *p // shallow copy to avoid mutating original
|
||||
|
||||
if cp.Err != nil {
|
||||
if _, ok := cp.Err.(encoding.TextMarshaler); !ok {
|
||||
cp.Err = &pluginError{cp.Err}
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal((*Alias)(&cp))
|
||||
}
|
||||
|
||||
// pluginCandidate represents a possible plugin candidate, for mocking purposes.
|
||||
type pluginCandidate interface {
|
||||
Path() string
|
||||
Metadata() ([]byte, error)
|
||||
}
|
||||
|
||||
// newPlugin determines if the given candidate is valid and returns a
|
||||
// Plugin. If the candidate fails one of the tests then `Plugin.Err`
|
||||
// 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 pluginCandidate, 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")
|
||||
@ -67,41 +45,43 @@ func newPlugin(c pluginCandidate, 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,
|
||||
}
|
||||
|
||||
// Now apply the candidate tests, so these update p.Err.
|
||||
if !isValidPluginName(p.Name) {
|
||||
p.Err = newPluginError("plugin candidate %q did not match %q", p.Name, pluginNameFormat)
|
||||
if !pluginNameRe.MatchString(p.Name) {
|
||||
p.Err = NewPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String())
|
||||
return p, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,80 +96,13 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
|
||||
p.Err = wrapAsPluginError(err, "invalid metadata")
|
||||
return p, nil
|
||||
}
|
||||
if err := validateSchemaVersion(p.Metadata.SchemaVersion); err != nil {
|
||||
p.Err = &pluginError{cause: err}
|
||||
if p.Metadata.SchemaVersion != "0.1.0" {
|
||||
p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion)
|
||||
return p, nil
|
||||
}
|
||||
if p.Metadata.Vendor == "" {
|
||||
p.Err = newPluginError("plugin metadata does not define a vendor")
|
||||
p.Err = NewPluginError("plugin metadata does not define a vendor")
|
||||
return p, nil
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// validateSchemaVersion validates if the plugin's schemaVersion is supported.
|
||||
//
|
||||
// The current schema-version is "0.1.0", but we don't want to break compatibility
|
||||
// until v2.0.0 of the schema version. Check for the major version to be < 2.0.0.
|
||||
//
|
||||
// Note that CLI versions before 28.4.1 may not support these versions as they were
|
||||
// hard-coded to only accept "0.1.0".
|
||||
func validateSchemaVersion(version string) error {
|
||||
if version == "0.1.0" {
|
||||
return nil
|
||||
}
|
||||
if version == "" {
|
||||
return errors.New("plugin SchemaVersion version cannot be empty")
|
||||
}
|
||||
major, _, ok := strings.Cut(version, ".")
|
||||
majorVersion, err := strconv.Atoi(major)
|
||||
if !ok || err != nil {
|
||||
return fmt.Errorf("plugin SchemaVersion %q has wrong format: must be <major>.<minor>.<patch>", version)
|
||||
}
|
||||
if majorVersion > 1 {
|
||||
return fmt.Errorf("plugin SchemaVersion %q is not supported: must be lower than 2.0.0", version)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunHook executes the plugin's hooks command
|
||||
// and returns its unprocessed output.
|
||||
func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte, error) {
|
||||
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
|
||||
}
|
||||
|
||||
// pluginNameFormat is used as part of errors for invalid plugin-names.
|
||||
// We should consider making this less technical ("must start with "a-z",
|
||||
// and only consist of lowercase alphanumeric characters").
|
||||
const pluginNameFormat = `^[a-z][a-z0-9]*$`
|
||||
|
||||
func isValidPluginName(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
// first character must be a-z
|
||||
if c := s[0]; c < 'a' || c > 'z' {
|
||||
return false
|
||||
}
|
||||
// followed by a-z or 0-9
|
||||
for i := 1; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if (c < 'a' || c > 'z') && (c < '0' || c > '9') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestPluginMarshal(t *testing.T) {
|
||||
const jsonWithError = `{"Name":"some-plugin","Err":"something went wrong"}`
|
||||
const jsonNoError = `{"Name":"some-plugin"}`
|
||||
|
||||
tests := []struct {
|
||||
doc string
|
||||
error error
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
doc: "no error",
|
||||
expected: jsonNoError,
|
||||
},
|
||||
{
|
||||
doc: "regular error",
|
||||
error: errors.New("something went wrong"),
|
||||
expected: jsonWithError,
|
||||
},
|
||||
{
|
||||
doc: "custom error",
|
||||
error: newPluginError("something went wrong"),
|
||||
expected: jsonWithError,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
actual, err := json.Marshal(&Plugin{Name: "some-plugin", Err: tc.error})
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(string(actual), tc.expected))
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package manager
|
||||
|
||||
func trimExeSuffix(s string) (string, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func addExeSuffix(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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"
|
||||
)
|
||||
@ -1,38 +0,0 @@
|
||||
package metadata
|
||||
|
||||
const (
|
||||
// NamePrefix is the prefix required on all plugin binary names
|
||||
NamePrefix = "docker-"
|
||||
|
||||
// MetadataSubcommandName is the name of the plugin subcommand
|
||||
// which must be supported by every plugin and returns the
|
||||
// plugin metadata.
|
||||
MetadataSubcommandName = "docker-cli-plugin-metadata"
|
||||
|
||||
// HookSubcommandName is the name of the plugin subcommand
|
||||
// which must be implemented by plugins declaring support
|
||||
// for hooks in their metadata.
|
||||
HookSubcommandName = "docker-cli-plugin-hooks"
|
||||
|
||||
// ReexecEnvvar is the name of an ennvar which is set to the command
|
||||
// used to originally invoke the docker CLI when executing a
|
||||
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
|
||||
// the plugin to re-execute the original CLI.
|
||||
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
|
||||
)
|
||||
|
||||
// Metadata provided by the plugin.
|
||||
type Metadata struct {
|
||||
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
|
||||
SchemaVersion string `json:",omitempty"`
|
||||
// Vendor is the name of the plugin vendor. Mandatory
|
||||
Vendor string `json:",omitempty"`
|
||||
// Version is the optional version of this plugin.
|
||||
Version string `json:",omitempty"`
|
||||
// ShortDescription should be suitable for a single line help message.
|
||||
ShortDescription string `json:",omitempty"`
|
||||
// URL is a pointer to the plugin's homepage.
|
||||
URL string `json:",omitempty"`
|
||||
// Hidden hides the plugin in completion and help message output.
|
||||
Hidden bool `json:",omitempty"`
|
||||
}
|
||||
@ -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/moby/moby/client"
|
||||
"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()
|
||||
@ -80,42 +51,37 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
|
||||
return cmd.Execute()
|
||||
}
|
||||
|
||||
// Run is the top-level entry point to the CLI plugin framework. It should
|
||||
// be called from the plugin's "main()" function. It initializes a new
|
||||
// [command.DockerCli] instance with the given options before calling
|
||||
// makeCmd to construct the plugin command, then invokes the plugin command
|
||||
// using [RunPlugin].
|
||||
func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata, ops ...command.CLIOption) {
|
||||
otel.SetErrorHandler(debug.OTELErrorHandler)
|
||||
|
||||
dockerCLI, err := command.NewDockerCli(ops...)
|
||||
// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
|
||||
func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
plugin := makeCmd(dockerCLI)
|
||||
plugin := makeCmd(dockerCli)
|
||||
|
||||
if err := RunPlugin(dockerCLI, plugin, meta); err != nil {
|
||||
var stErr cli.StatusError
|
||||
if errors.As(err, &stErr) {
|
||||
if err := RunPlugin(dockerCli, plugin, meta); err != nil {
|
||||
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 {
|
||||
return func(cli *command.DockerCli) error {
|
||||
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
|
||||
@ -137,19 +103,16 @@ func withPluginClientConn(name string) command.CLIOption {
|
||||
|
||||
helper, err := connhelper.GetCommandConnectionHelper(cmd, flags...)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
apiClient, err := client.New(client.WithDialContext(helper.Dialer))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return command.WithAPIClient(apiClient)(cli)
|
||||
}
|
||||
|
||||
return client.NewClientWithOpts(client.WithDialContext(helper.Dialer))
|
||||
})
|
||||
}
|
||||
|
||||
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),
|
||||
@ -162,52 +125,27 @@ 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, flags := cli.SetupPluginRootCommand(cmd)
|
||||
|
||||
// Disable file-completion by default. Most commands and flags should not
|
||||
// complete with filenames.
|
||||
cmd.CompletionOptions.SetDefaultShellCompDirective(cobra.ShellCompDirectiveNoFileComp)
|
||||
|
||||
opts, _ := cli.SetupPluginRootCommand(cmd)
|
||||
|
||||
cmd.SetIn(dockerCli.In())
|
||||
cmd.SetOut(dockerCli.Out())
|
||||
cmd.SetErr(dockerCli.Err())
|
||||
|
||||
cmd.AddCommand(
|
||||
plugin,
|
||||
newMetadataSubcommand(plugin, meta),
|
||||
)
|
||||
|
||||
visitAll(cmd,
|
||||
// prevent adding "[flags]" to the end of the usage line.
|
||||
func(c *cobra.Command) { c.DisableFlagsInUseLine = true },
|
||||
)
|
||||
cli.DisableFlagsInUseLine(cmd)
|
||||
|
||||
return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags())
|
||||
return cli.NewTopLevelCommand(cmd, dockerCli, opts, flags)
|
||||
}
|
||||
|
||||
// visitAll traverses all commands from the root.
|
||||
func visitAll(root *cobra.Command, fns ...func(*cobra.Command)) {
|
||||
for _, cmd := range root.Commands() {
|
||||
visitAll(cmd, fns...)
|
||||
}
|
||||
for _, fn := range fns {
|
||||
fn(root)
|
||||
}
|
||||
}
|
||||
|
||||
func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra.Command {
|
||||
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
|
||||
@ -222,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
|
||||
}
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestVisitAll(t *testing.T) {
|
||||
root := &cobra.Command{Use: "root"}
|
||||
sub1 := &cobra.Command{Use: "sub1"}
|
||||
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
|
||||
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
|
||||
sub2 := &cobra.Command{Use: "sub2"}
|
||||
|
||||
root.AddCommand(sub1, sub2)
|
||||
sub1.AddCommand(sub1sub1, sub1sub2)
|
||||
|
||||
var visited []string
|
||||
visitAll(root, func(ccmd *cobra.Command) {
|
||||
visited = append(visited, ccmd.Name())
|
||||
})
|
||||
expected := []string{"sub1sub1", "sub1sub2", "sub1", "sub2", "root"}
|
||||
if !slices.Equal(expected, visited) {
|
||||
t.Errorf("expected %#v, got %#v", expected, visited)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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))
|
||||
})
|
||||
}
|
||||
223
cli/cobra.go
223
cli/cobra.go
@ -3,37 +3,34 @@ 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"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// 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)
|
||||
@ -50,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
|
||||
@ -81,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,
|
||||
}
|
||||
}
|
||||
@ -100,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
|
||||
@ -161,11 +156,30 @@ 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...)
|
||||
}
|
||||
|
||||
// VisitAll will traverse all commands from the root.
|
||||
// This is different from the VisitAll of cobra.Command where only parents
|
||||
// are checked.
|
||||
func VisitAll(root *cobra.Command, fn func(*cobra.Command)) {
|
||||
for _, cmd := range root.Commands() {
|
||||
VisitAll(cmd, fn)
|
||||
}
|
||||
fn(root)
|
||||
}
|
||||
|
||||
// DisableFlagsInUseLine sets the DisableFlagsInUseLine flag on all
|
||||
// commands within the tree rooted at cmd.
|
||||
func DisableFlagsInUseLine(cmd *cobra.Command) {
|
||||
VisitAll(cmd, func(ccmd *cobra.Command) {
|
||||
// do not add a `[flags]` to the end of the usage line.
|
||||
ccmd.DisableFlagsInUseLine = true
|
||||
})
|
||||
}
|
||||
|
||||
var helpCommand = &cobra.Command{
|
||||
Use: "help [command]",
|
||||
Short: "Help about the command",
|
||||
@ -174,7 +188,7 @@ var helpCommand = &cobra.Command{
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
cmd, args, e := c.Root().Find(args)
|
||||
if cmd == nil || e != nil || len(args) > 0 {
|
||||
return fmt.Errorf("unknown help topic: %v", strings.Join(args, " "))
|
||||
return errors.Errorf("unknown help topic: %v", strings.Join(args, " "))
|
||||
}
|
||||
helpFunc := cmd.HelpFunc()
|
||||
helpFunc(cmd, args)
|
||||
@ -196,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 ""
|
||||
}
|
||||
@ -212,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 {
|
||||
@ -227,70 +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() + " "
|
||||
}
|
||||
var aliases strings.Builder
|
||||
aliases.WriteString(cmd.CommandPath())
|
||||
for _, alias := range cmd.Aliases {
|
||||
aliases.WriteString(", " + parentPath + alias)
|
||||
}
|
||||
return aliases.String()
|
||||
}
|
||||
|
||||
func topCommands(cmd *cobra.Command) []*cobra.Command {
|
||||
cmds := []*cobra.Command{}
|
||||
if cmd.Parent() != nil {
|
||||
// for now, only use top-commands for the root-command, and skip
|
||||
// for sub-commands
|
||||
return cmds
|
||||
}
|
||||
for _, sub := range cmd.Commands() {
|
||||
if isPlugin(sub) || !sub.IsAvailableCommand() {
|
||||
continue
|
||||
}
|
||||
if _, ok := sub.Annotations["category-top"]; ok {
|
||||
cmds = append(cmds, sub)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
@ -298,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)
|
||||
}
|
||||
@ -317,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)
|
||||
@ -328,33 +278,15 @@ 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 invalidPluginReason(sub) != "" {
|
||||
if isPlugin(sub) {
|
||||
if invalidPluginReason(sub) == "" {
|
||||
cmds = append(cmds, sub)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if sub.IsAvailableCommand() && (isPlugin(sub) || sub.HasSubCommands()) {
|
||||
if sub.IsAvailableCommand() && sub.HasSubCommands() {
|
||||
cmds = append(cmds, sub)
|
||||
}
|
||||
}
|
||||
@ -375,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}}
|
||||
@ -394,10 +326,10 @@ EXPERIMENTAL:
|
||||
https://docs.docker.com/go/experimental/
|
||||
|
||||
{{- end}}
|
||||
{{- if hasAliases . }}
|
||||
{{- if gt .Aliases 0}}
|
||||
|
||||
Aliases:
|
||||
{{ commandAliases . }}
|
||||
{{.NameAndAliases}}
|
||||
|
||||
{{- end}}
|
||||
{{- if .HasExample}}
|
||||
@ -406,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}}
|
||||
@ -456,14 +370,6 @@ Invalid Plugins:
|
||||
{{rpad .Name .NamePadding }} {{invalidPluginReason .}}
|
||||
{{- end}}
|
||||
|
||||
{{- end}}
|
||||
{{- if not .HasParent}}
|
||||
{{- if .HasAvailableFlags}}
|
||||
|
||||
Global Options:
|
||||
{{ wrappedFlagUsages . | trimRightSpace}}
|
||||
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{- if .HasSubCommands }}
|
||||
@ -473,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}}`
|
||||
|
||||
@ -3,13 +3,35 @@ 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"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestVisitAll(t *testing.T) {
|
||||
root := &cobra.Command{Use: "root"}
|
||||
sub1 := &cobra.Command{Use: "sub1"}
|
||||
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
|
||||
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
|
||||
sub2 := &cobra.Command{Use: "sub2"}
|
||||
|
||||
root.AddCommand(sub1, sub2)
|
||||
sub1.AddCommand(sub1sub1, sub1sub2)
|
||||
|
||||
// Take the opportunity to test DisableFlagsInUseLine too
|
||||
DisableFlagsInUseLine(root)
|
||||
|
||||
var visited []string
|
||||
VisitAll(root, func(ccmd *cobra.Command) {
|
||||
visited = append(visited, ccmd.Name())
|
||||
assert.Assert(t, ccmd.DisableFlagsInUseLine, "DisableFlagsInUseLine not set on %q", ccmd.Name())
|
||||
})
|
||||
expected := []string{"sub1sub1", "sub1sub2", "sub1", "sub2", "root"}
|
||||
assert.DeepEqual(t, expected, visited)
|
||||
}
|
||||
|
||||
func TestVendorAndVersion(t *testing.T) {
|
||||
// Non plugin.
|
||||
assert.Equal(t, vendorAndVersion(&cobra.Command{Use: "test"}), "")
|
||||
@ -27,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)
|
||||
@ -47,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)
|
||||
@ -56,55 +78,11 @@ func TestInvalidPlugin(t *testing.T) {
|
||||
assert.DeepEqual(t, invalidPlugins(root), []*cobra.Command{sub1}, cmpopts.IgnoreUnexported(cobra.Command{}))
|
||||
}
|
||||
|
||||
func TestHiddenPlugin(t *testing.T) {
|
||||
root := &cobra.Command{Use: "root"}
|
||||
sub1 := &cobra.Command{
|
||||
Use: "sub1",
|
||||
Hidden: true,
|
||||
Annotations: map[string]string{
|
||||
metadata.CommandAnnotationPlugin: "true",
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {},
|
||||
}
|
||||
|
||||
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
|
||||
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
|
||||
sub2 := &cobra.Command{
|
||||
Use: "sub2",
|
||||
Annotations: map[string]string{
|
||||
metadata.CommandAnnotationPlugin: "true",
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {},
|
||||
}
|
||||
|
||||
root.AddCommand(sub1, sub2)
|
||||
sub1.AddCommand(sub1sub1, sub1sub2)
|
||||
|
||||
assert.DeepEqual(t, allManagementSubCommands(root), []*cobra.Command{sub2}, cmpopts.IgnoreFields(cobra.Command{}, "Run"), cmpopts.IgnoreUnexported(cobra.Command{}))
|
||||
}
|
||||
|
||||
func TestCommandAliases(t *testing.T) {
|
||||
root := &cobra.Command{Use: "root"}
|
||||
sub := &cobra.Command{Use: "subcommand", Aliases: []string{"alias1", "alias2"}}
|
||||
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*")
|
||||
}
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
builderPruneFunc func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error)
|
||||
}
|
||||
|
||||
func (c *fakeClient) BuildCachePrune(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
|
||||
if c.builderPruneFunc != nil {
|
||||
return c.builderPruneFunc(ctx, opts)
|
||||
}
|
||||
return client.BuildCachePruneResult{}, nil
|
||||
}
|
||||
@ -6,52 +6,20 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/internal/commands"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands.Register(newBuilderCommand)
|
||||
commands.Register(func(c command.Cli) *cobra.Command {
|
||||
return newBakeStubCommand(c)
|
||||
})
|
||||
}
|
||||
|
||||
// newBuilderCommand returns a cobra command for `builder` subcommands
|
||||
func newBuilderCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
// NewBuilderCommand returns a cobra command for `builder` subcommands
|
||||
func NewBuilderCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "builder",
|
||||
Short: "Manage builds",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{"version": "1.31"},
|
||||
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newPruneCommand(dockerCLI),
|
||||
// we should have a mechanism for registering sub-commands in the cli/internal/commands.Register function.
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
image.NewBuildCommand(dockerCLI),
|
||||
NewPruneCommand(dockerCli),
|
||||
image.NewBuildCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// newBakeStubCommand returns a cobra command "stub" for the "bake" subcommand.
|
||||
// This command is a placeholder / stub that is dynamically replaced by an
|
||||
// alias for "docker buildx bake" if BuildKit is enabled (and the buildx plugin
|
||||
// installed).
|
||||
func newBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "bake [OPTIONS] [TARGET...]",
|
||||
Short: "Build from a file",
|
||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
||||
Annotations: map[string]string{
|
||||
// We want to show this command in the "top" category in --help
|
||||
// output, and not to be grouped under "management commands".
|
||||
"category-top": "5",
|
||||
"aliases": "docker buildx bake",
|
||||
"version": "1.31",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,36 +2,26 @@ package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/system/pruner"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register the prune command to run as part of "docker system prune"
|
||||
if err := pruner.Register(pruner.TypeBuildCache, pruneFn); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type pruneOptions struct {
|
||||
force bool
|
||||
all bool
|
||||
filter opts.FilterOpt
|
||||
reservedSpace opts.MemBytes
|
||||
force bool
|
||||
all bool
|
||||
filter opts.FilterOpt
|
||||
keepStorage opts.MemBytes
|
||||
}
|
||||
|
||||
// newPruneCommand returns a new cobra prune command for images
|
||||
func newPruneCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
// NewPruneCommand returns a new cobra prune command for images
|
||||
func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options := pruneOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -39,26 +29,24 @@ 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
|
||||
}
|
||||
if output != "" {
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), output)
|
||||
fmt.Fprintln(dockerCli.Out(), output)
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
|
||||
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
|
||||
return nil
|
||||
},
|
||||
Annotations: map[string]string{"version": "1.39"},
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
DisableFlagsInUseLine: true,
|
||||
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.reservedSpace, "keep-storage", "Amount of disk space to keep for cache")
|
||||
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
|
||||
}
|
||||
@ -68,32 +56,27 @@ 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) {
|
||||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
||||
func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
|
||||
pruneFilters := options.filter.Value()
|
||||
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
|
||||
|
||||
warning := normalWarning
|
||||
if options.all {
|
||||
warning = allCacheWarning
|
||||
}
|
||||
if !options.force {
|
||||
r, err := prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
if !r {
|
||||
return 0, "", cancelledErr{errors.New("builder prune has been cancelled")}
|
||||
}
|
||||
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
|
||||
return 0, "", nil
|
||||
}
|
||||
|
||||
resp, err := dockerCli.Client().BuildCachePrune(ctx, client.BuildCachePruneOptions{
|
||||
All: options.all,
|
||||
ReservedSpace: options.reservedSpace.Value(),
|
||||
Filters: pruneFilters,
|
||||
report, err := dockerCli.Client().BuildCachePrune(context.Background(), types.BuildCachePruneOptions{
|
||||
All: options.all,
|
||||
KeepStorage: options.keepStorage.Value(),
|
||||
Filters: pruneFilters,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
report := resp.Report
|
||||
|
||||
if len(report.CachesDeleted) > 0 {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Deleted build cache objects:\n")
|
||||
@ -107,26 +90,7 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
return report.SpaceReclaimed, output, nil
|
||||
}
|
||||
|
||||
type cancelledErr struct{ error }
|
||||
|
||||
func (cancelledErr) Cancelled() {}
|
||||
|
||||
// pruneFn prunes the build cache for use in "docker system prune" and
|
||||
// returns the amount of space reclaimed and a detailed output string.
|
||||
func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) {
|
||||
if !options.Confirmed {
|
||||
// Dry-run: perform validation and produce confirmation before pruning.
|
||||
var confirmMsg string
|
||||
if options.All {
|
||||
confirmMsg = "all build cache"
|
||||
} else {
|
||||
confirmMsg = "unused build cache"
|
||||
}
|
||||
return 0, confirmMsg, cancelledErr{errors.New("builder prune has been cancelled")}
|
||||
}
|
||||
return runPrune(ctx, dockerCLI, pruneOptions{
|
||||
force: true,
|
||||
all: options.All,
|
||||
filter: options.Filter,
|
||||
})
|
||||
// CachePrune executes a prune command for build cache
|
||||
func CachePrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
|
||||
return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter})
|
||||
}
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
func TestBuilderPromptTermination(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
builderPruneFunc: func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
|
||||
return client.BuildCachePruneResult{}, errors.New("fakeClient builderPruneFunc should not be called")
|
||||
},
|
||||
})
|
||||
cmd := newPruneCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
@ -3,33 +3,34 @@ package checkpoint
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
checkpointCreateFunc func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error)
|
||||
checkpointDeleteFunc func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error)
|
||||
checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error)
|
||||
checkpointCreateFunc func(container string, options 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 client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
|
||||
func (cli *fakeClient) CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error {
|
||||
if cli.checkpointCreateFunc != nil {
|
||||
return cli.checkpointCreateFunc(container, options)
|
||||
}
|
||||
return client.CheckpointCreateResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointRemove(_ context.Context, container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
|
||||
func (cli *fakeClient) CheckpointDelete(ctx context.Context, container string, options types.CheckpointDeleteOptions) error {
|
||||
if cli.checkpointDeleteFunc != nil {
|
||||
return cli.checkpointDeleteFunc(container, options)
|
||||
}
|
||||
return client.CheckpointRemoveResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointList(_ context.Context, container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
|
||||
func (cli *fakeClient) CheckpointList(ctx context.Context, container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
|
||||
if cli.checkpointListFunc != nil {
|
||||
return cli.checkpointListFunc(container, options)
|
||||
}
|
||||
return client.CheckpointListResult{}, nil
|
||||
return []types.Checkpoint{}, nil
|
||||
}
|
||||
|
||||
@ -3,32 +3,26 @@ package checkpoint
|
||||
import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/commands"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands.Register(newCheckpointCommand)
|
||||
}
|
||||
|
||||
// newCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
|
||||
func newCheckpointCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
// NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
|
||||
func NewCheckpointCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "checkpoint",
|
||||
Short: "Manage checkpoints",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{
|
||||
"experimental": "",
|
||||
"ostype": "linux",
|
||||
"version": "1.25",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newCreateCommand(dockerCLI),
|
||||
newListCommand(dockerCLI),
|
||||
newRemoveCommand(dockerCLI),
|
||||
newCreateCommand(dockerCli),
|
||||
newListCommand(dockerCli),
|
||||
newRemoveCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -17,7 +17,7 @@ type createOptions struct {
|
||||
leaveRunning bool
|
||||
}
|
||||
|
||||
func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
func newCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts createOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -27,29 +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: cobra.NoFileCompletions,
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
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, client.CheckpointCreateOptions{
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
package checkpoint
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"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 client.CheckpointCreateOptions) (client.CheckpointCreateResult, 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 client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
|
||||
return client.CheckpointCreateResult{}, 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 client.CheckpointCreateOptions
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
|
||||
actualContainerName = container
|
||||
actualOptions = options
|
||||
return client.CheckpointCreateResult{}, 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 := client.CheckpointCreateOptions{
|
||||
CheckpointID: checkpointName,
|
||||
CheckpointDir: checkpointDir,
|
||||
Exit: !tc,
|
||||
}
|
||||
assert.Check(t, is.Equal(actualOptions, expected))
|
||||
assert.Check(t, is.Equal(strings.TrimSpace(cli.OutBuffer().String()), checkpointName))
|
||||
})
|
||||
}
|
||||
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())))
|
||||
}
|
||||
|
||||
@ -2,44 +2,48 @@ package checkpoint
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/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 checkpointContext.
|
||||
func newFormat(source string) formatter.Format {
|
||||
if source == formatter.TableFormatKey {
|
||||
// NewFormat returns a format for use with a checkpoint Context
|
||||
func NewFormat(source string) formatter.Format {
|
||||
switch source {
|
||||
case formatter.TableFormatKey:
|
||||
return defaultCheckpointFormat
|
||||
}
|
||||
return formatter.Format(source)
|
||||
}
|
||||
|
||||
// formatWrite writes formatted checkpoints using the Context
|
||||
func formatWrite(fmtCtx formatter.Context, checkpoints []checkpoint.Summary) error {
|
||||
cpContext := &checkpointContext{
|
||||
HeaderContext: formatter.HeaderContext{
|
||||
Header: formatter.SubHeaderContext{
|
||||
"Name": checkpointNameHeader,
|
||||
},
|
||||
},
|
||||
}
|
||||
return fmtCtx.Write(cpContext, func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, cp := range checkpoints {
|
||||
if err := format(&checkpointContext{c: cp}); err != nil {
|
||||
// FormatWrite writes formatted checkpoints using the Context
|
||||
func FormatWrite(ctx formatter.Context, checkpoints []types.Checkpoint) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, checkpoint := range checkpoints {
|
||||
if err := format(&checkpointContext{c: checkpoint}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return ctx.Write(newCheckpointContext(), render)
|
||||
}
|
||||
|
||||
type checkpointContext struct {
|
||||
formatter.HeaderContext
|
||||
c checkpoint.Summary
|
||||
c types.Checkpoint
|
||||
}
|
||||
|
||||
func newCheckpointContext() *checkpointContext {
|
||||
cpCtx := checkpointContext{}
|
||||
cpCtx.Header = formatter.SubHeaderContext{
|
||||
"Name": checkpointNameHeader,
|
||||
}
|
||||
return &cpCtx
|
||||
}
|
||||
|
||||
func (c *checkpointContext) MarshalJSON() ([]byte, error) {
|
||||
|
||||
@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/checkpoint"
|
||||
"github.com/docker/docker/api/types"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@ -15,7 +15,7 @@ func TestCheckpointContextFormatWrite(t *testing.T) {
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
formatter.Context{Format: newFormat(defaultCheckpointFormat)},
|
||||
formatter.Context{Format: NewFormat(defaultCheckpointFormat)},
|
||||
`CHECKPOINT NAME
|
||||
checkpoint-1
|
||||
checkpoint-2
|
||||
@ -23,14 +23,14 @@ checkpoint-3
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: newFormat("{{.Name}}")},
|
||||
formatter.Context{Format: NewFormat("{{.Name}}")},
|
||||
`checkpoint-1
|
||||
checkpoint-2
|
||||
checkpoint-3
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: newFormat("{{.Name}}:")},
|
||||
formatter.Context{Format: NewFormat("{{.Name}}:")},
|
||||
`checkpoint-1:
|
||||
checkpoint-2:
|
||||
checkpoint-3:
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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/moby/moby/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -15,7 +14,7 @@ type listOptions struct {
|
||||
checkpointDir string
|
||||
}
|
||||
|
||||
func newListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts listOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -24,29 +23,32 @@ 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),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
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, client.CheckpointListOptions{
|
||||
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
|
||||
}
|
||||
|
||||
cpCtx := formatter.Context{
|
||||
Output: dockerCLI.Out(),
|
||||
Format: newFormat(formatter.TableFormatKey),
|
||||
Output: dockerCli.Out(),
|
||||
Format: NewFormat(formatter.TableFormatKey),
|
||||
}
|
||||
return formatWrite(cpCtx, checkpoints.Items)
|
||||
return FormatWrite(cpCtx, checkpoints)
|
||||
}
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
package checkpoint
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/api/types/checkpoint"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
@ -16,21 +15,21 @@ import (
|
||||
func TestCheckpointListErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error)
|
||||
checkpointListFunc func(container string, options 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 client.CheckpointListOptions) (client.CheckpointListResult, error) {
|
||||
return client.CheckpointListResult{}, 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",
|
||||
},
|
||||
@ -42,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)
|
||||
}
|
||||
}
|
||||
@ -51,19 +49,17 @@ func TestCheckpointListErrors(t *testing.T) {
|
||||
func TestCheckpointListWithOptions(t *testing.T) {
|
||||
var containerID, checkpointDir string
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
|
||||
checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
|
||||
containerID = container
|
||||
checkpointDir = options.CheckpointDir
|
||||
return client.CheckpointListResult{
|
||||
Items: []checkpoint.Summary{
|
||||
{Name: "checkpoint-foo"},
|
||||
},
|
||||
return []types.Checkpoint{
|
||||
{Name: "checkpoint-foo"},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs([]string{"container-foo"})
|
||||
assert.Check(t, cmd.Flags().Set("checkpoint-dir", "/dir/foo"))
|
||||
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal("container-foo", containerID))
|
||||
assert.Check(t, is.Equal("/dir/foo", checkpointDir))
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
package checkpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -11,7 +13,7 @@ type removeOptions struct {
|
||||
checkpointDir string
|
||||
}
|
||||
|
||||
func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts removeOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -20,18 +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 {
|
||||
containerID, checkpointID := args[0], args[1]
|
||||
_, err := dockerCLI.Client().CheckpointRemove(cmd.Context(), containerID, client.CheckpointRemoveOptions{
|
||||
CheckpointID: checkpointID,
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
})
|
||||
return err
|
||||
return runRemove(dockerCli, args[0], args[1], opts)
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
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(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)
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
package checkpoint
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"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 client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, 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 client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
|
||||
return client.CheckpointRemoveResult{}, 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,16 +48,16 @@ func TestCheckpointRemoveErrors(t *testing.T) {
|
||||
func TestCheckpointRemoveWithOptions(t *testing.T) {
|
||||
var containerID, checkpointID, checkpointDir string
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointDeleteFunc: func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
|
||||
checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error {
|
||||
containerID = container
|
||||
checkpointID = options.CheckpointID
|
||||
checkpointDir = options.CheckpointDir
|
||||
return client.CheckpointRemoveResult{}, nil
|
||||
return nil
|
||||
},
|
||||
})
|
||||
cmd := newRemoveCommand(cli)
|
||||
cmd.SetArgs([]string{"container-foo", "checkpoint-bar"})
|
||||
assert.Check(t, cmd.Flags().Set("checkpoint-dir", "/dir/foo"))
|
||||
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal("container-foo", containerID))
|
||||
assert.Check(t, is.Equal("checkpoint-bar", checkpointID))
|
||||
|
||||
@ -1,104 +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.24
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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/moby/moby/api/types/build"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
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)
|
||||
config.Provider
|
||||
Apply(ops ...DockerCliOption) error
|
||||
ConfigFile() *configfile.ConfigFile
|
||||
ServerInfo() ServerInfo
|
||||
CurrentVersion() string
|
||||
BuildKitEnabled() (bool, error)
|
||||
ClientInfo() ClientInfo
|
||||
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||
DefaultVersion() string
|
||||
ManifestStore() manifeststore.Store
|
||||
RegistryClient(bool) registryclient.RegistryClient
|
||||
ContentTrustEnabled() bool
|
||||
ContextStore() store.Store
|
||||
CurrentContext() string
|
||||
StackOrchestrator(flagValue string) (Orchestrator, error)
|
||||
DockerEndpoint() docker.Endpoint
|
||||
TelemetryClient
|
||||
}
|
||||
|
||||
// DockerCli is an instance the docker command line client.
|
||||
// Instances of the client should be created using the [NewDockerCli]
|
||||
// constructor to make sure they are properly initialized with defaults
|
||||
// set.
|
||||
// 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
|
||||
userAgent string
|
||||
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
|
||||
contextStoreConfig store.Config
|
||||
}
|
||||
|
||||
// CurrentVersion returns the API version currently negotiated, or the default
|
||||
// version otherwise.
|
||||
func (cli *DockerCli) CurrentVersion() string {
|
||||
_ = cli.initialize()
|
||||
if cli.client == nil {
|
||||
return client.MaxAPIVersion
|
||||
}
|
||||
return 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
|
||||
}
|
||||
|
||||
@ -108,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
|
||||
}
|
||||
|
||||
@ -133,196 +125,189 @@ 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
|
||||
}
|
||||
|
||||
// BuildKitEnabled returns buildkit is enabled or not.
|
||||
func (cli *DockerCli) BuildKitEnabled() (bool, error) {
|
||||
// use DOCKER_BUILDKIT env var value if set and not empty
|
||||
if v := os.Getenv("DOCKER_BUILDKIT"); v != "" {
|
||||
enabled, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("DOCKER_BUILDKIT environment variable expects boolean value: %w", err)
|
||||
// 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 enabled, nil
|
||||
}
|
||||
// if a builder alias is defined, we are using BuildKit
|
||||
aliasMap := cli.ConfigFile().Aliases
|
||||
if _, ok := aliasMap["builder"]; ok {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
si := cli.ServerInfo()
|
||||
if si.BuildkitVersion == build.BuilderBuildKit {
|
||||
// The daemon advertised BuildKit as the preferred builder; this may
|
||||
// be either a Linux daemon or a Windows daemon with experimental
|
||||
// BuildKit support enabled.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// otherwise, assume BuildKit is enabled for Linux, but disabled for
|
||||
// Windows / WCOW, which does not yet support BuildKit by default.
|
||||
return si.OSType != "windows", nil
|
||||
return *cli.clientInfo
|
||||
}
|
||||
|
||||
// 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
|
||||
func (cli *DockerCli) loadClientInfo() error {
|
||||
var v string
|
||||
if cli.client != nil {
|
||||
v = cli.client.ClientVersion()
|
||||
} else {
|
||||
v = api.DefaultVersion
|
||||
}
|
||||
// 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
|
||||
cli.clientInfo = &ClientInfo{
|
||||
DefaultVersion: v,
|
||||
HasExperimental: true,
|
||||
}
|
||||
featuresMap := cli.ConfigFile().Features
|
||||
if v, ok := featuresMap["hooks"]; ok {
|
||||
enabled, err := strconv.ParseBool(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContentTrustEnabled returns whether content trust has been enabled by an
|
||||
// environment variable.
|
||||
func (cli *DockerCli) ContentTrustEnabled() bool {
|
||||
return cli.contentTrust
|
||||
}
|
||||
|
||||
// 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
|
||||
return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
// default to false
|
||||
return false
|
||||
return buildkitEnabled, nil
|
||||
}
|
||||
|
||||
// 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)) InitializeOpt {
|
||||
return func(dockerCli *DockerCli) error {
|
||||
var err error
|
||||
dockerCli.client, err = makeClient(dockerCli)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 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.loadConfigFile()
|
||||
|
||||
baseContextStore := store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig)
|
||||
cli.contextStore = &ContextStoreWithDefault{
|
||||
Store: baseContextStore,
|
||||
Resolver: func() (*DefaultContext, error) {
|
||||
return ResolveDefaultContext(opts.Common, cli.ConfigFile(), cli.contextStoreConfig, cli.Err())
|
||||
},
|
||||
}
|
||||
cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli.dockerEndpoint, err = resolveDockerEndpoint(cli.contextStore, cli.currentContext)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to resolve docker endpoint")
|
||||
}
|
||||
|
||||
if cli.contextStoreConfig == nil {
|
||||
// This path can be hit when calling Initialize on a DockerCli that's
|
||||
// not constructed through [NewDockerCli]. Using the default context
|
||||
// store without a config set will result in Endpoints from contexts
|
||||
// not being type-mapped correctly, and used as a generic "map[string]any",
|
||||
// instead of a [docker.EndpointMeta].
|
||||
//
|
||||
// When looking up the API endpoint (using [EndpointFromContext]), no
|
||||
// endpoint will be found, and a default, empty endpoint will be used
|
||||
// instead which in its turn, causes newAPIClientFromEndpoint to
|
||||
// be initialized with the default config instead of settings for
|
||||
// the current context (which may mean; connecting with the wrong
|
||||
// endpoint and/or TLS Config to be missing).
|
||||
//
|
||||
// [EndpointFromContext]: https://github.com/docker/cli/blob/33494921b80fd0b5a06acc3a34fa288de4bb2e6b/cli/context/docker/load.go#L139-L149
|
||||
if err := WithDefaultContextStoreConfig()(cli); err != nil {
|
||||
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 //nolint: staticcheck // SA1019: cli.dockerEndpoint.TLSPassword is deprecated
|
||||
return newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile)
|
||||
}
|
||||
cli.client, err = getClientWithPassword(passRetriever, newClient)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
cli.initializeFromClient()
|
||||
|
||||
cli.options = opts
|
||||
cli.configFile = config.LoadDefaultConfigFile(cli.err)
|
||||
cli.currentContext = resolveContextName(cli.options, cli.configFile)
|
||||
cli.contextStore = &ContextStoreWithDefault{
|
||||
Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig),
|
||||
Resolver: func() (*DefaultContext, error) {
|
||||
return resolveDefaultContext(cli.options, *cli.contextStoreConfig)
|
||||
},
|
||||
}
|
||||
|
||||
// TODO(krissetto): pass ctx to the funcs instead of using this
|
||||
if cli.enableGlobalMeter {
|
||||
cli.createGlobalMeterProvider(cli.baseCtx)
|
||||
}
|
||||
if cli.enableGlobalTracer {
|
||||
cli.createGlobalTracerProvider(cli.baseCtx)
|
||||
}
|
||||
filterResourceAttributesEnvvar()
|
||||
|
||||
// early return if GODEBUG is already set or the docker context is
|
||||
// the default context, i.e. is a virtual context where we won't override
|
||||
// any GODEBUG values.
|
||||
if v := os.Getenv("GODEBUG"); cli.currentContext == DefaultContextName || v != "" {
|
||||
return nil
|
||||
}
|
||||
meta, err := cli.contextStore.GetMetadata(cli.currentContext)
|
||||
if err == nil {
|
||||
setGoDebug(meta)
|
||||
if err := cli.loadClientInfo(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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, fmt.Errorf("unable to resolve docker endpoint: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
return newAPIClientFromEndpoint(endpoint, configFile, client.WithUserAgent(UserAgent()))
|
||||
endpoint, err := resolveDockerEndpoint(store, contextName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to resolve docker endpoint")
|
||||
}
|
||||
return newAPIClientFromEndpoint(endpoint, configFile)
|
||||
}
|
||||
|
||||
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile, extraOpts ...client.Opt) (client.APIClient, error) {
|
||||
opts, err := ep.ClientOpts()
|
||||
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
|
||||
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
|
||||
}
|
||||
withCustomHeaders, err := withCustomHeadersFromEnv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if withCustomHeaders != nil {
|
||||
opts = append(opts, withCustomHeaders)
|
||||
}
|
||||
opts = append(opts, extraOpts...)
|
||||
return client.New(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
|
||||
@ -335,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
|
||||
}
|
||||
@ -366,32 +348,52 @@ 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, client.PingOptions{
|
||||
NegotiateAPIVersion: true,
|
||||
ForceNegotiate: true,
|
||||
})
|
||||
ping, err := cli.client.Ping(ctx)
|
||||
if err != nil {
|
||||
// Default to true if we fail to connect to daemon
|
||||
cli.serverInfo = ServerInfo{HasExperimental: true}
|
||||
|
||||
if ping.APIVersion != "" {
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
cli.serverInfo = ServerInfo{
|
||||
HasExperimental: ping.Experimental,
|
||||
OSType: ping.OSType,
|
||||
BuildkitVersion: ping.BuilderVersion,
|
||||
SwarmStatus: ping.SwarmStatus,
|
||||
}
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
}
|
||||
|
||||
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
|
||||
@ -399,151 +401,43 @@ 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)
|
||||
// Apply all the operation on the cli
|
||||
func (cli *DockerCli) Apply(ops ...DockerCliOption) error {
|
||||
for _, op := range ops {
|
||||
if err := op(cli); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return resolveDockerEndpoint(cli.contextStore, cn)
|
||||
}
|
||||
|
||||
// setGoDebug is an escape hatch that sets the GODEBUG environment
|
||||
// variable value using docker context metadata.
|
||||
//
|
||||
// {
|
||||
// "Name": "my-context",
|
||||
// "Metadata": { "GODEBUG": "x509negativeserial=1" }
|
||||
// }
|
||||
//
|
||||
// WARNING: Setting x509negativeserial=1 allows Go's x509 library to accept
|
||||
// X.509 certificates with negative serial numbers.
|
||||
// This behavior is deprecated and non-compliant with current security
|
||||
// standards (RFC 5280). Accepting negative serial numbers can introduce
|
||||
// serious security vulnerabilities, including the risk of certificate
|
||||
// collision or bypass attacks.
|
||||
// This option should only be used for legacy compatibility and never in
|
||||
// production environments.
|
||||
// Use at your own risk.
|
||||
func setGoDebug(meta store.Metadata) {
|
||||
fieldName := "GODEBUG"
|
||||
godebugEnv := os.Getenv(fieldName)
|
||||
// early return if GODEBUG is already set. We don't want to override what
|
||||
// the user already sets.
|
||||
if godebugEnv != "" {
|
||||
return
|
||||
}
|
||||
|
||||
var cfg any
|
||||
var ok bool
|
||||
switch m := meta.Metadata.(type) {
|
||||
case DockerContext:
|
||||
cfg, ok = m.AdditionalFields[fieldName]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
case map[string]any:
|
||||
cfg, ok = m[fieldName]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
v, ok := cfg.(string)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// set the GODEBUG environment variable with whatever was in the context
|
||||
_ = os.Setenv(fieldName, v)
|
||||
}
|
||||
|
||||
func (cli *DockerCli) initialize() error {
|
||||
cli.init.Do(func() {
|
||||
cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint()
|
||||
if cli.initErr != nil {
|
||||
cli.initErr = fmt.Errorf("unable to resolve docker endpoint: %w", cli.initErr)
|
||||
return
|
||||
}
|
||||
if cli.client == nil {
|
||||
ops := []client.Opt{client.WithUserAgent(cli.userAgent)}
|
||||
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile, ops...); cli.initErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if cli.baseCtx == nil {
|
||||
cli.baseCtx = context.Background()
|
||||
}
|
||||
cli.initializeFromClient()
|
||||
})
|
||||
return cli.initErr
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerInfo stores details about the supported features and platform of the
|
||||
@ -551,56 +445,100 @@ func (cli *DockerCli) initialize() error {
|
||||
type ServerInfo struct {
|
||||
HasExperimental bool
|
||||
OSType string
|
||||
BuildkitVersion build.BuilderVersion
|
||||
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 *client.SwarmStatus
|
||||
// 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{
|
||||
WithDefaultContextStoreConfig(),
|
||||
WithStandardStreams(),
|
||||
WithUserAgent(UserAgent()),
|
||||
func NewDockerCli(ops ...DockerCliOption) (*DockerCli, error) {
|
||||
cli := &DockerCli{}
|
||||
defaultOps := []DockerCliOption{
|
||||
WithContentTrustFromEnv(),
|
||||
}
|
||||
cli.contextStoreConfig = DefaultContextStoreConfig()
|
||||
ops = append(defaultOps, ops...)
|
||||
|
||||
cli := &DockerCli{baseCtx: context.Background()}
|
||||
for _, op := range ops {
|
||||
if err := op(cli); err != nil {
|
||||
return nil, err
|
||||
if err := cli.Apply(ops...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 default user agent string used for making API requests.
|
||||
// UserAgent returns the user agent string used for making API requests
|
||||
func UserAgent() string {
|
||||
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -614,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...,
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,58 +1,43 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/cli/cli/context/docker"
|
||||
"github.com/docker/cli/cli/context/store"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/moby/term"
|
||||
)
|
||||
|
||||
// CLIOption is a functional argument to apply options to a [DockerCli]. These
|
||||
// options can be passed to [NewDockerCli] to initialize a new CLI, or
|
||||
// applied with [DockerCli.Initialize] or [DockerCli.Apply].
|
||||
type CLIOption func(cli *DockerCli) error
|
||||
// DockerCliOption applies a modification on a DockerCli.
|
||||
type DockerCliOption func(cli *DockerCli) error
|
||||
|
||||
// WithStandardStreams sets a cli in, out and err streams with the standard streams.
|
||||
func WithStandardStreams() CLIOption {
|
||||
func WithStandardStreams() DockerCliOption {
|
||||
return func(cli *DockerCli) error {
|
||||
// Set terminal emulation based on platform as required.
|
||||
stdin, stdout, stderr := term.StdStreams()
|
||||
cli.in = streams.NewIn(stdin)
|
||||
cli.out = streams.NewOut(stdout)
|
||||
cli.err = streams.NewOut(stderr)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithBaseContext sets the base context of a cli. It is used to propagate
|
||||
// the context from the command line to the client.
|
||||
func WithBaseContext(ctx context.Context) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.baseCtx = ctx
|
||||
cli.err = stderr
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithCombinedStreams uses the same stream for the output and error streams.
|
||||
func WithCombinedStreams(combined io.Writer) CLIOption {
|
||||
func WithCombinedStreams(combined io.Writer) DockerCliOption {
|
||||
return func(cli *DockerCli) error {
|
||||
s := streams.NewOut(combined)
|
||||
cli.out = s
|
||||
cli.err = s
|
||||
cli.out = streams.NewOut(combined)
|
||||
cli.err = combined
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithInputStream sets a cli input stream.
|
||||
func WithInputStream(in io.ReadCloser) CLIOption {
|
||||
func WithInputStream(in io.ReadCloser) DockerCliOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.in = streams.NewIn(in)
|
||||
return nil
|
||||
@ -60,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
|
||||
@ -68,160 +53,44 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultContextStoreConfig configures the cli to use the default context store configuration.
|
||||
func WithDefaultContextStoreConfig() CLIOption {
|
||||
// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
|
||||
func WithContentTrustFromEnv() DockerCliOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cfg := DefaultContextStoreConfig()
|
||||
cli.contextStoreConfig = &cfg
|
||||
cli.contentTrust = false
|
||||
if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" {
|
||||
if t, err := strconv.ParseBool(e); t || err != nil {
|
||||
// treat any other value as true
|
||||
cli.contentTrust = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAPIClient configures the cli to use the given API client.
|
||||
func WithAPIClient(c client.APIClient) CLIOption {
|
||||
// WithContentTrust enables content trust on a cli.
|
||||
func WithContentTrust(enabled bool) DockerCliOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.client = c
|
||||
cli.contentTrust = enabled
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithInitializeClient is passed to [DockerCli.Initialize] to initialize
|
||||
// an API Client for use by the CLI.
|
||||
func WithInitializeClient(makeClient func(*DockerCli) (client.APIClient, error)) CLIOption {
|
||||
// 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 {
|
||||
c, err := makeClient(cli)
|
||||
if err != nil {
|
||||
return err
|
||||
switch endpointName {
|
||||
case docker.DockerEndpoint:
|
||||
return fmt.Errorf("cannot change %q endpoint type", endpointName)
|
||||
}
|
||||
return WithAPIClient(c)(cli)
|
||||
}
|
||||
}
|
||||
|
||||
// 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, error) {
|
||||
value := os.Getenv(envOverrideHTTPHeaders)
|
||||
if value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return nil, invalidParameter(fmt.Errorf(
|
||||
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
|
||||
envOverrideHTTPHeaders,
|
||||
))
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
env := map[string]string{}
|
||||
for _, kv := range fields {
|
||||
k, v, hasValue := strings.Cut(kv, "=")
|
||||
|
||||
// Only strip whitespace in keys; preserve whitespace in values.
|
||||
k = strings.TrimSpace(k)
|
||||
|
||||
if k == "" {
|
||||
return nil, invalidParameter(fmt.Errorf(
|
||||
`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
}
|
||||
|
||||
// We don't currently allow empty key=value pairs, and produce an error.
|
||||
// This is something we could allow in future (e.g. to read value
|
||||
// from an environment variable with the same name). In the meantime,
|
||||
// produce an error to prevent users from depending on this.
|
||||
if !hasValue {
|
||||
return nil, invalidParameter(fmt.Errorf(
|
||||
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
}
|
||||
|
||||
env[http.CanonicalHeaderKey(k)] = v
|
||||
}
|
||||
|
||||
if len(env) == 0 {
|
||||
// We should probably not hit this case, as we don't skip values
|
||||
// (only return errors), but we don't want to discard existing
|
||||
// headers with an empty set.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
|
||||
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
|
||||
return client.WithHTTPHeaders(env), nil
|
||||
}
|
||||
|
||||
// WithUserAgent configures the User-Agent string for cli HTTP requests.
|
||||
func WithUserAgent(userAgent string) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
if userAgent == "" {
|
||||
return errors.New("user agent cannot be blank")
|
||||
}
|
||||
cli.userAgent = userAgent
|
||||
cli.contextStoreConfig.SetEndpoint(endpointName, endpointType)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
37
cli/command/cli_options_test.go
Normal file
37
cli/command/cli_options_test.go
Normal file
@ -0,0 +1,37 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func contentTrustEnabled(t *testing.T) bool {
|
||||
var cli DockerCli
|
||||
assert.NilError(t, WithContentTrustFromEnv()(&cli))
|
||||
return cli.contentTrust
|
||||
}
|
||||
|
||||
// NB: Do not t.Parallel() this test -- it messes with the process environment.
|
||||
func TestWithContentTrustFromEnv(t *testing.T) {
|
||||
envvar := "DOCKER_CONTENT_TRUST"
|
||||
if orig, ok := os.LookupEnv(envvar); ok {
|
||||
defer func() {
|
||||
os.Setenv(envvar, orig)
|
||||
}()
|
||||
} else {
|
||||
defer func() {
|
||||
os.Unsetenv(envvar)
|
||||
}()
|
||||
}
|
||||
|
||||
os.Setenv(envvar, "true")
|
||||
assert.Assert(t, contentTrustEnabled(t))
|
||||
os.Setenv(envvar, "false")
|
||||
assert.Assert(t, !contentTrustEnabled(t))
|
||||
os.Setenv(envvar, "invalid")
|
||||
assert.Assert(t, contentTrustEnabled(t))
|
||||
os.Unsetenv(envvar)
|
||||
assert.Assert(t, !contentTrustEnabled(t))
|
||||
}
|
||||
@ -3,25 +3,24 @@ package command
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"io/ioutil"
|
||||
"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/context/store"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/env"
|
||||
"gotest.tools/v3/fs"
|
||||
)
|
||||
|
||||
func TestNewAPIClientFromFlags(t *testing.T) {
|
||||
@ -29,262 +28,270 @@ 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(), client.MaxAPIVersion)
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
|
||||
host := ":2375"
|
||||
slug := "tcp://localhost"
|
||||
if runtime.GOOS == "windows" {
|
||||
slug = "tcp://127.0.0.1"
|
||||
}
|
||||
opts := &flags.ClientOptions{Hosts: []string{host}}
|
||||
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, apiClient.DaemonHost(), slug+host)
|
||||
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
|
||||
var received map[string]string
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
received = map[string]string{
|
||||
"My-Header": r.Header.Get("My-Header"),
|
||||
"User-Agent": r.Header.Get("User-Agent"),
|
||||
}
|
||||
_, _ = w.Write([]byte("OK"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
|
||||
opts := &flags.ClientOptions{Hosts: []string{host}}
|
||||
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(), client.MaxAPIVersion)
|
||||
|
||||
// verify User-Agent is not appended to the configfile. see https://github.com/docker/cli/pull/2756
|
||||
assert.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"})
|
||||
assert.Check(t, is.Equal(host, apiclient.DaemonHost()))
|
||||
|
||||
expectedHeaders := map[string]string{
|
||||
"My-Header": "Custom-Value",
|
||||
"User-Agent": UserAgent(),
|
||||
}
|
||||
_, err = apiClient.Ping(context.TODO(), client.PingOptions{})
|
||||
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(), client.MaxAPIVersion)
|
||||
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.TODO(), client.PingOptions{})
|
||||
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"
|
||||
const expectedVersion = "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()))
|
||||
}
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
pingFunc func() (client.PingResult, error)
|
||||
pingFunc func() (types.Ping, error)
|
||||
version string
|
||||
negotiated bool
|
||||
}
|
||||
|
||||
func (c *fakeClient) Ping(_ context.Context, options client.PingOptions) (client.PingResult, error) {
|
||||
res, err := c.pingFunc()
|
||||
if options.NegotiateAPIVersion {
|
||||
if res.APIVersion != "" {
|
||||
if c.negotiated || options.ForceNegotiate {
|
||||
c.negotiated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return res, err
|
||||
func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) {
|
||||
return c.pingFunc()
|
||||
}
|
||||
|
||||
func (c *fakeClient) ClientVersion() string {
|
||||
return c.version
|
||||
}
|
||||
|
||||
func TestInitializeFromClient(t *testing.T) {
|
||||
const defaultVersion = "v1.55"
|
||||
func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
|
||||
c.negotiated = true
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
func TestInitializeFromClient(t *testing.T) {
|
||||
defaultVersion := "v1.55"
|
||||
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
pingFunc func() (client.PingResult, error)
|
||||
pingFunc func() (types.Ping, error)
|
||||
expectedServer ServerInfo
|
||||
negotiated bool
|
||||
}{
|
||||
{
|
||||
doc: "successful ping",
|
||||
pingFunc: func() (client.PingResult, error) {
|
||||
return client.PingResult{Experimental: true, OSType: "linux", APIVersion: "v1.44"}, nil
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
|
||||
negotiated: true,
|
||||
},
|
||||
{
|
||||
doc: "failed ping, no API version",
|
||||
pingFunc: func() (client.PingResult, error) {
|
||||
return client.PingResult{}, errors.New("failed")
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{}, errors.New("failed")
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true},
|
||||
},
|
||||
{
|
||||
doc: "failed ping, with API version",
|
||||
pingFunc: func() (client.PingResult, error) {
|
||||
return client.PingResult{APIVersion: "v1.44"}, errors.New("failed")
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{APIVersion: "v1.33"}, errors.New("failed")
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true},
|
||||
negotiated: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
apiClient := &fakeClient{
|
||||
pingFunc: tc.pingFunc,
|
||||
for _, testcase := range testcases {
|
||||
testcase := testcase
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
apiclient := &fakeClient{
|
||||
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 := &DockerCli{client: apiclient}
|
||||
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) {
|
||||
tmpDir := t.TempDir()
|
||||
socket := filepath.Join(tmpDir, "my.sock")
|
||||
l, err := net.Listen("unix", socket)
|
||||
assert.NilError(t, err)
|
||||
// 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"
|
||||
|
||||
receiveReqCh := make(chan bool)
|
||||
timeoutCtx, cancel := context.WithTimeout(context.TODO(), 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:
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
configfile string
|
||||
}{
|
||||
{
|
||||
doc: "default",
|
||||
configfile: `{}`,
|
||||
},
|
||||
{
|
||||
doc: "experimental",
|
||||
configfile: `{
|
||||
"experimental": "enabled"
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
select {
|
||||
case <-timeoutCtx.Done():
|
||||
t.Fatal("server never received an init request")
|
||||
case <-receiveReqCh:
|
||||
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,
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{Experimental: true, OSType: "linux", APIVersion: defaultVersion}, nil
|
||||
},
|
||||
}
|
||||
|
||||
cli := &DockerCli{client: apiclient, err: os.Stderr}
|
||||
cliconfig.SetDir(dir.Path())
|
||||
err := cli.Initialize(flags.NewClientOptions())
|
||||
assert.NilError(t, err)
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDockerCliAndOperators(t *testing.T) {
|
||||
outbuf := bytes.NewBuffer(nil)
|
||||
errbuf := bytes.NewBuffer(nil)
|
||||
|
||||
// Test default operations and also overriding default ones
|
||||
cli, err := NewDockerCli(
|
||||
WithInputStream(io.NopCloser(strings.NewReader("some input"))),
|
||||
WithOutputStream(outbuf),
|
||||
WithErrorStream(errbuf),
|
||||
WithContentTrust(true),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
// Check streams are initialized
|
||||
assert.Check(t, cli.In() != nil)
|
||||
assert.Check(t, cli.Out() != nil)
|
||||
assert.Check(t, cli.Err() != nil)
|
||||
inputStream, err := io.ReadAll(cli.In())
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(inputStream), "some input")
|
||||
assert.Equal(t, cli.ContentTrustEnabled(), true)
|
||||
|
||||
// Check output stream
|
||||
_, err = fmt.Fprint(cli.Out(), "output")
|
||||
// Apply can modify a dockerCli after construction
|
||||
inbuf := bytes.NewBuffer([]byte("input"))
|
||||
outbuf := bytes.NewBuffer(nil)
|
||||
errbuf := bytes.NewBuffer(nil)
|
||||
err = cli.Apply(
|
||||
WithInputStream(ioutil.NopCloser(inbuf)),
|
||||
WithOutputStream(outbuf),
|
||||
WithErrorStream(errbuf),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
outputStream, err := io.ReadAll(outbuf)
|
||||
// Check input stream
|
||||
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 := ioutil.ReadAll(outbuf)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(outputStream), "output")
|
||||
// Check error stream
|
||||
_, err = fmt.Fprint(cli.Err(), "error")
|
||||
assert.NilError(t, err)
|
||||
errStream, err := io.ReadAll(errbuf)
|
||||
fmt.Fprintf(cli.Err(), "error")
|
||||
errStream, err := ioutil.ReadAll(errbuf)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(errStream), "error")
|
||||
}
|
||||
@ -292,103 +299,8 @@ func TestNewDockerCliAndOperators(t *testing.T) {
|
||||
func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
apiClient, err := client.New()
|
||||
assert.NilError(t, err)
|
||||
assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithAPIClient(apiClient)))
|
||||
assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithInitializeClient(func(cli *DockerCli) (client.APIClient, error) {
|
||||
return client.NewClientWithOpts()
|
||||
})))
|
||||
assert.Check(t, cli.ContextStore() != nil)
|
||||
}
|
||||
|
||||
func TestHooksEnabled(t *testing.T) {
|
||||
t.Run("disabled by default", func(t *testing.T) {
|
||||
// Make sure we don't depend on any existing ~/.docker/config.json
|
||||
config.SetDir(t.TempDir())
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Check(t, !cli.HooksEnabled())
|
||||
})
|
||||
|
||||
t.Run("enabled in configFile", func(t *testing.T) {
|
||||
configFile := `{
|
||||
"features": {
|
||||
"hooks": "true"
|
||||
}}`
|
||||
config.SetDir(t.TempDir())
|
||||
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
|
||||
assert.NilError(t, err)
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, cli.HooksEnabled())
|
||||
})
|
||||
|
||||
t.Run("env var overrides configFile", func(t *testing.T) {
|
||||
configFile := `{
|
||||
"features": {
|
||||
"hooks": "true"
|
||||
}}`
|
||||
t.Setenv("DOCKER_CLI_HOOKS", "false")
|
||||
config.SetDir(t.TempDir())
|
||||
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
|
||||
assert.NilError(t, err)
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, !cli.HooksEnabled())
|
||||
})
|
||||
|
||||
t.Run("legacy env var overrides configFile", func(t *testing.T) {
|
||||
configFile := `{
|
||||
"features": {
|
||||
"hooks": "true"
|
||||
}}`
|
||||
t.Setenv("DOCKER_CLI_HINTS", "false")
|
||||
config.SetDir(t.TempDir())
|
||||
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
|
||||
assert.NilError(t, err)
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, !cli.HooksEnabled())
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetGoDebug(t *testing.T) {
|
||||
t.Run("GODEBUG already set", func(t *testing.T) {
|
||||
t.Setenv("GODEBUG", "val1,val2")
|
||||
meta := store.Metadata{}
|
||||
setGoDebug(meta)
|
||||
assert.Equal(t, "val1,val2", os.Getenv("GODEBUG"))
|
||||
})
|
||||
t.Run("GODEBUG in context metadata can set env", func(t *testing.T) {
|
||||
meta := store.Metadata{
|
||||
Metadata: DockerContext{
|
||||
AdditionalFields: map[string]any{
|
||||
"GODEBUG": "val1,val2=1",
|
||||
},
|
||||
},
|
||||
}
|
||||
setGoDebug(meta)
|
||||
assert.Equal(t, "val1,val2=1", os.Getenv("GODEBUG"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewDockerCliWithCustomUserAgent(t *testing.T) {
|
||||
var received string
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
received = r.UserAgent()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer ts.Close()
|
||||
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
|
||||
opts := &flags.ClientOptions{Hosts: []string{host}}
|
||||
|
||||
cli, err := NewDockerCli(
|
||||
WithUserAgent("fake-agent/0.0.1"),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
cli.currentContext = DefaultContextName
|
||||
cli.options = opts
|
||||
cli.configFile = &configfile.ConfigFile{}
|
||||
|
||||
_, err = cli.Client().Ping(context.TODO(), client.PingOptions{})
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, received, "fake-agent/0.0.1")
|
||||
}
|
||||
|
||||
@ -1,30 +1,139 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
_ "github.com/docker/cli/cli/command/builder"
|
||||
_ "github.com/docker/cli/cli/command/checkpoint"
|
||||
_ "github.com/docker/cli/cli/command/config"
|
||||
_ "github.com/docker/cli/cli/command/container"
|
||||
_ "github.com/docker/cli/cli/command/context"
|
||||
_ "github.com/docker/cli/cli/command/image"
|
||||
_ "github.com/docker/cli/cli/command/manifest"
|
||||
_ "github.com/docker/cli/cli/command/network"
|
||||
_ "github.com/docker/cli/cli/command/node"
|
||||
_ "github.com/docker/cli/cli/command/plugin"
|
||||
_ "github.com/docker/cli/cli/command/registry"
|
||||
_ "github.com/docker/cli/cli/command/secret"
|
||||
_ "github.com/docker/cli/cli/command/service"
|
||||
_ "github.com/docker/cli/cli/command/stack"
|
||||
_ "github.com/docker/cli/cli/command/swarm"
|
||||
_ "github.com/docker/cli/cli/command/system"
|
||||
_ "github.com/docker/cli/cli/command/volume"
|
||||
"github.com/docker/cli/internal/commands"
|
||||
"github.com/docker/cli/cli/command/builder"
|
||||
"github.com/docker/cli/cli/command/checkpoint"
|
||||
"github.com/docker/cli/cli/command/config"
|
||||
"github.com/docker/cli/cli/command/container"
|
||||
"github.com/docker/cli/cli/command/context"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/command/manifest"
|
||||
"github.com/docker/cli/cli/command/network"
|
||||
"github.com/docker/cli/cli/command/node"
|
||||
"github.com/docker/cli/cli/command/plugin"
|
||||
"github.com/docker/cli/cli/command/registry"
|
||||
"github.com/docker/cli/cli/command/secret"
|
||||
"github.com/docker/cli/cli/command/service"
|
||||
"github.com/docker/cli/cli/command/stack"
|
||||
"github.com/docker/cli/cli/command/swarm"
|
||||
"github.com/docker/cli/cli/command/system"
|
||||
"github.com/docker/cli/cli/command/trust"
|
||||
"github.com/docker/cli/cli/command/volume"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func AddCommands(cmd *cobra.Command, dockerCLI command.Cli) {
|
||||
for _, c := range commands.Commands() {
|
||||
cmd.AddCommand(c(dockerCLI))
|
||||
}
|
||||
// AddCommands adds all the commands from cli/command to the root command
|
||||
func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
|
||||
cmd.AddCommand(
|
||||
// checkpoint
|
||||
checkpoint.NewCheckpointCommand(dockerCli),
|
||||
|
||||
// config
|
||||
config.NewConfigCommand(dockerCli),
|
||||
|
||||
// container
|
||||
container.NewContainerCommand(dockerCli),
|
||||
container.NewRunCommand(dockerCli),
|
||||
|
||||
// image
|
||||
image.NewImageCommand(dockerCli),
|
||||
image.NewBuildCommand(dockerCli),
|
||||
|
||||
// builder
|
||||
builder.NewBuilderCommand(dockerCli),
|
||||
|
||||
// manifest
|
||||
manifest.NewManifestCommand(dockerCli),
|
||||
|
||||
// network
|
||||
network.NewNetworkCommand(dockerCli),
|
||||
|
||||
// node
|
||||
node.NewNodeCommand(dockerCli),
|
||||
|
||||
// plugin
|
||||
plugin.NewPluginCommand(dockerCli),
|
||||
|
||||
// registry
|
||||
registry.NewLoginCommand(dockerCli),
|
||||
registry.NewLogoutCommand(dockerCli),
|
||||
registry.NewSearchCommand(dockerCli),
|
||||
|
||||
// secret
|
||||
secret.NewSecretCommand(dockerCli),
|
||||
|
||||
// service
|
||||
service.NewServiceCommand(dockerCli),
|
||||
|
||||
// system
|
||||
system.NewSystemCommand(dockerCli),
|
||||
system.NewVersionCommand(dockerCli),
|
||||
|
||||
// stack
|
||||
stack.NewStackCommand(dockerCli),
|
||||
|
||||
// swarm
|
||||
swarm.NewSwarmCommand(dockerCli),
|
||||
|
||||
// trust
|
||||
trust.NewTrustCommand(dockerCli),
|
||||
|
||||
// volume
|
||||
volume.NewVolumeCommand(dockerCli),
|
||||
|
||||
// context
|
||||
context.NewContextCommand(dockerCli),
|
||||
|
||||
// legacy commands may be hidden
|
||||
hide(system.NewEventsCommand(dockerCli)),
|
||||
hide(system.NewInfoCommand(dockerCli)),
|
||||
hide(system.NewInspectCommand(dockerCli)),
|
||||
hide(container.NewAttachCommand(dockerCli)),
|
||||
hide(container.NewCommitCommand(dockerCli)),
|
||||
hide(container.NewCopyCommand(dockerCli)),
|
||||
hide(container.NewCreateCommand(dockerCli)),
|
||||
hide(container.NewDiffCommand(dockerCli)),
|
||||
hide(container.NewExecCommand(dockerCli)),
|
||||
hide(container.NewExportCommand(dockerCli)),
|
||||
hide(container.NewKillCommand(dockerCli)),
|
||||
hide(container.NewLogsCommand(dockerCli)),
|
||||
hide(container.NewPauseCommand(dockerCli)),
|
||||
hide(container.NewPortCommand(dockerCli)),
|
||||
hide(container.NewPsCommand(dockerCli)),
|
||||
hide(container.NewRenameCommand(dockerCli)),
|
||||
hide(container.NewRestartCommand(dockerCli)),
|
||||
hide(container.NewRmCommand(dockerCli)),
|
||||
hide(container.NewStartCommand(dockerCli)),
|
||||
hide(container.NewStatsCommand(dockerCli)),
|
||||
hide(container.NewStopCommand(dockerCli)),
|
||||
hide(container.NewTopCommand(dockerCli)),
|
||||
hide(container.NewUnpauseCommand(dockerCli)),
|
||||
hide(container.NewUpdateCommand(dockerCli)),
|
||||
hide(container.NewWaitCommand(dockerCli)),
|
||||
hide(image.NewHistoryCommand(dockerCli)),
|
||||
hide(image.NewImagesCommand(dockerCli)),
|
||||
hide(image.NewImportCommand(dockerCli)),
|
||||
hide(image.NewLoadCommand(dockerCli)),
|
||||
hide(image.NewPullCommand(dockerCli)),
|
||||
hide(image.NewPushCommand(dockerCli)),
|
||||
hide(image.NewRemoveCommand(dockerCli)),
|
||||
hide(image.NewSaveCommand(dockerCli)),
|
||||
hide(image.NewTagCommand(dockerCli)),
|
||||
)
|
||||
}
|
||||
|
||||
func hide(cmd *cobra.Command) *cobra.Command {
|
||||
// If the environment variable with name "DOCKER_HIDE_LEGACY_COMMANDS" is not empty,
|
||||
// these legacy commands (such as `docker ps`, `docker exec`, etc)
|
||||
// will not be shown in output console.
|
||||
if os.Getenv("DOCKER_HIDE_LEGACY_COMMANDS") == "" {
|
||||
return cmd
|
||||
}
|
||||
cmdCopy := *cmd
|
||||
cmdCopy.Hidden = true
|
||||
cmdCopy.Aliases = []string{}
|
||||
return &cmdCopy
|
||||
}
|
||||
|
||||
@ -1,220 +0,0 @@
|
||||
package completion
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// APIClientProvider provides a method to get a [client.APIClient], initializing
|
||||
// it if needed.
|
||||
//
|
||||
// It's a smaller interface than [command.Cli], and used in situations where an
|
||||
// APIClient is needed, but we want to postpone initializing the client until
|
||||
// it's used.
|
||||
type APIClientProvider interface {
|
||||
Client() client.APIClient
|
||||
}
|
||||
|
||||
// ImageNames offers completion for images present within the local store
|
||||
func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if limit > 0 && len(args) >= limit {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, img := range res.Items {
|
||||
names = append(names, img.RepoTags...)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// ImageNamesWithBase offers completion for images present within the local store,
|
||||
// including both full image names with tags and base image names (repository names only)
|
||||
// when multiple tags exist for the same base name
|
||||
func ImageNamesWithBase(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if limit > 0 && len(args) >= limit {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
baseNameCounts := make(map[string]int)
|
||||
for _, img := range res.Items {
|
||||
names = append(names, img.RepoTags...)
|
||||
for _, tag := range img.RepoTags {
|
||||
ref, err := reference.ParseNormalizedNamed(tag)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
baseNameCounts[reference.FamiliarName(ref)]++
|
||||
}
|
||||
}
|
||||
for baseName, count := range baseNameCounts {
|
||||
if count > 1 {
|
||||
names = append(names, baseName)
|
||||
}
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// ContainerNames offers completion for container names and IDs
|
||||
// By default, only names are returned.
|
||||
// Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs.
|
||||
func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(container.Summary) bool) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
res, err := dockerCLI.Client().ContainerList(cmd.Context(), client.ContainerListOptions{
|
||||
All: all,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
|
||||
showContainerIDs := os.Getenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS") == "yes"
|
||||
|
||||
var names []string
|
||||
for _, ctr := range res.Items {
|
||||
skip := false
|
||||
for _, fn := range filters {
|
||||
if fn != nil && !fn(ctr) {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if skip {
|
||||
continue
|
||||
}
|
||||
if showContainerIDs {
|
||||
names = append(names, ctr.ID)
|
||||
}
|
||||
names = append(names, formatter.StripNamePrefix(ctr.Names)...)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// VolumeNames offers completion for volumes
|
||||
func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
res, err := dockerCLI.Client().VolumeList(cmd.Context(), client.VolumeListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, vol := range res.Items {
|
||||
names = append(names, vol.Name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkNames offers completion for networks
|
||||
func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
res, err := dockerCLI.Client().NetworkList(cmd.Context(), client.NetworkListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, nw := range res.Items {
|
||||
names = append(names, nw.Name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// EnvVarNames offers completion for environment-variable names. This
|
||||
// completion can be used for "--env" and "--build-arg" flags, which
|
||||
// allow obtaining the value of the given environment-variable if present
|
||||
// in the local environment, so we only should complete the names of the
|
||||
// environment variables, and not their value. This also prevents the
|
||||
// completion script from printing values of environment variables
|
||||
// containing sensitive values.
|
||||
//
|
||||
// For example;
|
||||
//
|
||||
// export MY_VAR=hello
|
||||
// docker run --rm --env MY_VAR alpine printenv MY_VAR
|
||||
// hello
|
||||
func EnvVarNames() cobra.CompletionFunc {
|
||||
return func(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
|
||||
envs := os.Environ()
|
||||
names = make([]string, 0, len(envs))
|
||||
for _, env := range envs {
|
||||
name, _, _ := strings.Cut(env, "=")
|
||||
names = append(names, name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// FromList offers completion for the given list of options.
|
||||
func FromList(options ...string) cobra.CompletionFunc {
|
||||
return 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.CompletionFunc {
|
||||
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
}
|
||||
|
||||
var commonPlatforms = []string{
|
||||
"linux/386",
|
||||
"linux/amd64",
|
||||
"linux/arm",
|
||||
"linux/arm/v5",
|
||||
"linux/arm/v6",
|
||||
"linux/arm/v7",
|
||||
"linux/arm64",
|
||||
"linux/arm64/v8",
|
||||
|
||||
// IBM power and z platforms
|
||||
"linux/ppc64le",
|
||||
"linux/s390x",
|
||||
|
||||
// Not yet supported
|
||||
"linux/riscv64",
|
||||
|
||||
"windows/amd64",
|
||||
|
||||
"wasip1/wasm",
|
||||
}
|
||||
|
||||
// Platforms offers completion for platform-strings. It provides a non-exhaustive
|
||||
// list of platforms to be used for completion. Platform-strings are based on
|
||||
// [runtime.GOOS] and [runtime.GOARCH], but with (optional) variants added. A
|
||||
// list of recognised os/arch combinations from the Go runtime can be obtained
|
||||
// through "go tool dist list".
|
||||
//
|
||||
// Some noteworthy exclusions from this list:
|
||||
//
|
||||
// - arm64 images ("windows/arm64", "windows/arm64/v8") do not yet exist for windows.
|
||||
// - we don't (yet) include `os-variant` for completion (as can be used for Windows images)
|
||||
// - we don't (yet) include platforms for which we don't build binaries, such as
|
||||
// BSD platforms (freebsd, netbsd, openbsd), android, macOS (darwin).
|
||||
// - we currently exclude architectures that may have unofficial builds,
|
||||
// but don't have wide adoption (and no support), such as loong64, mipsXXX,
|
||||
// ppc64 (non-le) to prevent confusion.
|
||||
func Platforms() cobra.CompletionFunc {
|
||||
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
@ -1,353 +0,0 @@
|
||||
package completion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/image"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
"github.com/moby/moby/api/types/volume"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/env"
|
||||
)
|
||||
|
||||
type fakeCLI struct {
|
||||
*fakeClient
|
||||
}
|
||||
|
||||
// Client implements [APIClientProvider].
|
||||
func (c fakeCLI) Client() client.APIClient {
|
||||
return c.fakeClient
|
||||
}
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
containerListFunc func(context.Context, client.ContainerListOptions) (client.ContainerListResult, error)
|
||||
imageListFunc func(context.Context, client.ImageListOptions) (client.ImageListResult, error)
|
||||
networkListFunc func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error)
|
||||
volumeListFunc func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error)
|
||||
}
|
||||
|
||||
func (c *fakeClient) ContainerList(ctx context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
if c.containerListFunc != nil {
|
||||
return c.containerListFunc(ctx, options)
|
||||
}
|
||||
return client.ContainerListResult{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ImageList(ctx context.Context, options client.ImageListOptions) (client.ImageListResult, error) {
|
||||
if c.imageListFunc != nil {
|
||||
return c.imageListFunc(ctx, options)
|
||||
}
|
||||
return client.ImageListResult{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) NetworkList(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) {
|
||||
if c.networkListFunc != nil {
|
||||
return c.networkListFunc(ctx, options)
|
||||
}
|
||||
return client.NetworkListResult{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) VolumeList(ctx context.Context, options client.VolumeListOptions) (client.VolumeListResult, error) {
|
||||
if c.volumeListFunc != nil {
|
||||
return c.volumeListFunc(ctx, options)
|
||||
}
|
||||
return client.VolumeListResult{}, nil
|
||||
}
|
||||
|
||||
func TestCompleteContainerNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
showAll, showIDs bool
|
||||
filters []func(container.Summary) bool
|
||||
containers []container.Summary
|
||||
expOut []string
|
||||
expOpts client.ContainerListOptions
|
||||
expDirective cobra.ShellCompDirective
|
||||
}{
|
||||
{
|
||||
doc: "no results",
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "all containers",
|
||||
showAll: true,
|
||||
containers: []container.Summary{
|
||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
||||
},
|
||||
expOut: []string{"container-c", "container-c/link-b", "container-b", "container-a"},
|
||||
expOpts: client.ContainerListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "all containers with ids",
|
||||
showAll: true,
|
||||
showIDs: true,
|
||||
containers: []container.Summary{
|
||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
||||
},
|
||||
expOut: []string{"id-c", "container-c", "container-c/link-b", "id-b", "container-b", "id-a", "container-a"},
|
||||
expOpts: client.ContainerListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "only running containers",
|
||||
showAll: false,
|
||||
containers: []container.Summary{
|
||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
||||
},
|
||||
expOut: []string{"container-c", "container-c/link-b"},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "with filter",
|
||||
showAll: true,
|
||||
filters: []func(container.Summary) bool{
|
||||
func(ctr container.Summary) bool { return ctr.State == container.StateCreated },
|
||||
},
|
||||
containers: []container.Summary{
|
||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
||||
},
|
||||
expOut: []string{"container-b"},
|
||||
expOpts: client.ContainerListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "multiple filters",
|
||||
showAll: true,
|
||||
filters: []func(container.Summary) bool{
|
||||
func(ctr container.Summary) bool { return ctr.ID == "id-a" },
|
||||
func(ctr container.Summary) bool { return ctr.State == container.StateCreated },
|
||||
},
|
||||
containers: []container.Summary{
|
||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
||||
{ID: "id-a", State: container.StateCreated, Names: []string{"/container-a"}},
|
||||
},
|
||||
expOut: []string{"container-a"},
|
||||
expOpts: client.ContainerListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "with error",
|
||||
expDirective: cobra.ShellCompDirectiveError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
if tc.showIDs {
|
||||
t.Setenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS", "yes")
|
||||
}
|
||||
comp := ContainerNames(fakeCLI{&fakeClient{
|
||||
containerListFunc: func(_ context.Context, opts client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
assert.Check(t, is.DeepEqual(opts, tc.expOpts))
|
||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||
return client.ContainerListResult{}, errors.New("some error occurred")
|
||||
}
|
||||
return client.ContainerListResult{Items: tc.containers}, nil
|
||||
},
|
||||
}}, tc.showAll, tc.filters...)
|
||||
|
||||
containers, directives := comp(&cobra.Command{}, nil, "")
|
||||
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
||||
assert.Check(t, is.DeepEqual(containers, tc.expOut))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteEnvVarNames(t *testing.T) {
|
||||
env.PatchAll(t, map[string]string{
|
||||
"ENV_A": "hello-a",
|
||||
"ENV_B": "hello-b",
|
||||
})
|
||||
values, directives := EnvVarNames()(nil, nil, "")
|
||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
||||
|
||||
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(context.Context, client.ImageListOptions) (client.ImageListResult, error) {
|
||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||
return client.ImageListResult{}, errors.New("some error occurred")
|
||||
}
|
||||
return client.ImageListResult{Items: tc.images}, nil
|
||||
},
|
||||
}}, -1)
|
||||
|
||||
volumes, directives := comp(&cobra.Command{}, nil, "")
|
||||
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
||||
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteNetworkNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
networks []network.Summary
|
||||
expOut []string
|
||||
expDirective cobra.ShellCompDirective
|
||||
}{
|
||||
{
|
||||
doc: "no results",
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "with results",
|
||||
networks: []network.Summary{
|
||||
{
|
||||
Network: network.Network{
|
||||
ID: "nw-c",
|
||||
Name: "network-c",
|
||||
},
|
||||
},
|
||||
{
|
||||
Network: network.Network{
|
||||
ID: "nw-b",
|
||||
Name: "network-b",
|
||||
},
|
||||
},
|
||||
{
|
||||
Network: network.Network{
|
||||
ID: "nw-a",
|
||||
Name: "network-a",
|
||||
},
|
||||
},
|
||||
},
|
||||
expOut: []string{"network-c", "network-b", "network-a"},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "with error",
|
||||
expDirective: cobra.ShellCompDirectiveError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
comp := NetworkNames(fakeCLI{&fakeClient{
|
||||
networkListFunc: func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error) {
|
||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||
return client.NetworkListResult{}, errors.New("some error occurred")
|
||||
}
|
||||
return client.NetworkListResult{Items: tc.networks}, nil
|
||||
},
|
||||
}})
|
||||
|
||||
volumes, directives := comp(&cobra.Command{}, nil, "")
|
||||
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
||||
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompletePlatforms(t *testing.T) {
|
||||
values, directives := Platforms()(nil, nil, "")
|
||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
||||
assert.Check(t, is.DeepEqual(values, commonPlatforms))
|
||||
}
|
||||
|
||||
func TestCompleteVolumeNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
volumes []volume.Volume
|
||||
expOut []string
|
||||
expDirective cobra.ShellCompDirective
|
||||
}{
|
||||
{
|
||||
doc: "no results",
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "with results",
|
||||
volumes: []volume.Volume{
|
||||
{Name: "volume-c"},
|
||||
{Name: "volume-b"},
|
||||
{Name: "volume-a"},
|
||||
},
|
||||
expOut: []string{"volume-c", "volume-b", "volume-a"},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "with error",
|
||||
expDirective: cobra.ShellCompDirectiveError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
comp := VolumeNames(fakeCLI{&fakeClient{
|
||||
volumeListFunc: func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error) {
|
||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||
return client.VolumeListResult{}, errors.New("some error occurred")
|
||||
}
|
||||
return client.VolumeListResult{Items: tc.volumes}, nil
|
||||
},
|
||||
}})
|
||||
|
||||
volumes, directives := comp(&cobra.Command{}, nil, "")
|
||||
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
||||
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -3,41 +3,43 @@ package config
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error)
|
||||
configInspectFunc func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
configListFunc func(context.Context, client.ConfigListOptions) (client.ConfigListResult, error)
|
||||
configRemoveFunc func(context.Context, string, client.ConfigRemoveOptions) (client.ConfigRemoveResult, error)
|
||||
configCreateFunc func(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, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
func (c *fakeClient) ConfigCreate(ctx context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
if c.configCreateFunc != nil {
|
||||
return c.configCreateFunc(ctx, options)
|
||||
return c.configCreateFunc(spec)
|
||||
}
|
||||
return client.ConfigCreateResult{}, nil
|
||||
return types.ConfigCreateResponse{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigInspect(ctx context.Context, id string, options client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
|
||||
if c.configInspectFunc != nil {
|
||||
return c.configInspectFunc(ctx, id, options)
|
||||
return c.configInspectFunc(id)
|
||||
}
|
||||
return client.ConfigInspectResult{}, nil
|
||||
return swarm.Config{}, nil, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigList(ctx context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
func (c *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
if c.configListFunc != nil {
|
||||
return c.configListFunc(ctx, options)
|
||||
return c.configListFunc(options)
|
||||
}
|
||||
return client.ConfigListResult{}, nil
|
||||
return []swarm.Config{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigRemove(ctx context.Context, name string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
||||
func (c *fakeClient) ConfigRemove(ctx context.Context, name string) error {
|
||||
if c.configRemoveFunc != nil {
|
||||
return c.configRemoveFunc(ctx, name, options)
|
||||
return c.configRemoveFunc(name)
|
||||
}
|
||||
return client.ConfigRemoveResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1,51 +1,29 @@
|
||||
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/cli/internal/commands"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands.Register(newConfigCommand)
|
||||
}
|
||||
|
||||
// newConfigCommand returns a cobra command for `config` subcommands
|
||||
func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
// NewConfigCommand returns a cobra command for `config` subcommands
|
||||
func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Manage Swarm configs",
|
||||
Short: "Manage Docker configs",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{
|
||||
"version": "1.30",
|
||||
"swarm": "manager",
|
||||
"swarm": "",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newConfigListCommand(dockerCLI),
|
||||
newConfigCreateCommand(dockerCLI),
|
||||
newConfigInspectCommand(dockerCLI),
|
||||
newConfigRemoveCommand(dockerCLI),
|
||||
newConfigListCommand(dockerCli),
|
||||
newConfigCreateCommand(dockerCli),
|
||||
newConfigInspectCommand(dockerCli),
|
||||
newConfigRemoveCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// completeNames offers completion for swarm configs
|
||||
func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
res, err := dockerCLI.Client().ConfigList(cmd.Context(), client.ConfigListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, config := range res.Items {
|
||||
names = append(names, config.ID)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,30 +2,30 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/moby/sys/sequential"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// createOptions specifies some options that are used when creating a config.
|
||||
type createOptions struct {
|
||||
name string
|
||||
templateDriver string
|
||||
file string
|
||||
labels opts.ListOpts
|
||||
// CreateOptions specifies some options that are used when creating a config.
|
||||
type CreateOptions struct {
|
||||
Name string
|
||||
TemplateDriver string
|
||||
File string
|
||||
Labels opts.ListOpts
|
||||
}
|
||||
|
||||
func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
createOpts := createOptions{
|
||||
labels: opts.NewListOpts(opts.ValidateLabel),
|
||||
func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
createOpts := CreateOptions{
|
||||
Labels: opts.NewListOpts(opts.ValidateLabel),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -33,113 +33,56 @@ func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Short: "Create a config from a file or STDIN",
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
createOpts.name = args[0]
|
||||
createOpts.file = args[1]
|
||||
return runCreate(cmd.Context(), dockerCLI, createOpts)
|
||||
createOpts.Name = args[0]
|
||||
createOpts.File = args[1]
|
||||
return RunConfigCreate(dockerCli, createOpts)
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
switch len(args) {
|
||||
case 0:
|
||||
// No completion for the first argument, which is the name for
|
||||
// the new config, but if a non-empty name is given, we return
|
||||
// it as completion to allow "tab"-ing to the next completion.
|
||||
return []string{toComplete}, cobra.ShellCompDirectiveNoFileComp
|
||||
case 1:
|
||||
// Second argument is either "-" or a file to load.
|
||||
//
|
||||
// TODO(thaJeztah): provide completion for "-".
|
||||
return nil, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveDefault
|
||||
default:
|
||||
// Command only accepts two arguments.
|
||||
return nil, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.VarP(&createOpts.labels, "label", "l", "Config labels")
|
||||
flags.StringVar(&createOpts.templateDriver, "template-driver", "", "Template driver")
|
||||
_ = flags.SetAnnotation("template-driver", "version", []string{"1.37"})
|
||||
flags.VarP(&createOpts.Labels, "label", "l", "Config labels")
|
||||
flags.StringVar(&createOpts.TemplateDriver, "template-driver", "", "Template driver")
|
||||
flags.SetAnnotation("template-driver", "version", []string{"1.37"})
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// runCreate creates a config with the given options.
|
||||
func runCreate(ctx context.Context, dockerCLI command.Cli, options createOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
// RunConfigCreate creates a config with the given options.
|
||||
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 fmt.Errorf("error reading content from %q: %v", options.file, err)
|
||||
return errors.Errorf("Error reading content from %q: %v", options.File, err)
|
||||
}
|
||||
|
||||
spec := swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: options.name,
|
||||
Labels: opts.ConvertKVStringsToMap(options.labels.GetSlice()),
|
||||
Name: options.Name,
|
||||
Labels: opts.ConvertKVStringsToMap(options.Labels.GetAll()),
|
||||
},
|
||||
Data: configData,
|
||||
}
|
||||
if options.templateDriver != "" {
|
||||
if options.TemplateDriver != "" {
|
||||
spec.Templating = &swarm.Driver{
|
||||
Name: options.templateDriver,
|
||||
Name: options.TemplateDriver,
|
||||
}
|
||||
}
|
||||
r, err := apiClient.ConfigCreate(ctx, client.ConfigCreateOptions{
|
||||
Spec: 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,16 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
"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,53 +21,49 @@ const configDataFile = "config-create-with-name.golden"
|
||||
func TestConfigCreateErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error)
|
||||
configCreateFunc func(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, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
return client.ConfigCreateResult{}, 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, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
if options.Spec.Name != name {
|
||||
return client.ConfigCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name)
|
||||
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
if spec.Name != name {
|
||||
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
|
||||
}
|
||||
|
||||
actual = options.Spec.Data
|
||||
actual = spec.Data
|
||||
|
||||
return client.ConfigCreateResult{
|
||||
ID: "ID-" + options.Spec.Name,
|
||||
return types.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -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,13 +94,13 @@ func TestConfigCreateWithLabels(t *testing.T) {
|
||||
}
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
if !reflect.DeepEqual(options.Spec, expected) {
|
||||
return client.ConfigCreateResult{}, fmt.Errorf("expected %+v, got %+v", expected, options.Spec)
|
||||
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
if !reflect.DeepEqual(spec, expected) {
|
||||
return types.ConfigCreateResponse{}, errors.Errorf("expected %+v, got %+v", expected, spec)
|
||||
}
|
||||
|
||||
return client.ConfigCreateResult{
|
||||
ID: "ID-" + options.Spec.Name,
|
||||
return types.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -124,20 +117,20 @@ 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, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
if options.Spec.Name != name {
|
||||
return client.ConfigCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name)
|
||||
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
if spec.Name != name {
|
||||
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
|
||||
}
|
||||
|
||||
if options.Spec.Templating.Name != expectedDriver.Name {
|
||||
return client.ConfigCreateResult{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, options.Spec.Labels)
|
||||
if spec.Templating.Name != expectedDriver.Name {
|
||||
return types.ConfigCreateResponse{}, errors.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
|
||||
}
|
||||
|
||||
return client.ConfigCreateResult{
|
||||
ID: "ID-" + options.Spec.Name,
|
||||
return types.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
@ -5,11 +5,11 @@ 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/go-units"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
units "github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -30,8 +30,8 @@ Data:
|
||||
{{.Data}}`
|
||||
)
|
||||
|
||||
// newFormat returns a Format for rendering using a configContext.
|
||||
func newFormat(source string, quiet bool) formatter.Format {
|
||||
// NewFormat returns a Format for rendering using a config Context
|
||||
func NewFormat(source string, quiet bool) formatter.Format {
|
||||
switch source {
|
||||
case formatter.PrettyFormatKey:
|
||||
return configInspectPrettyTemplate
|
||||
@ -44,28 +44,31 @@ func newFormat(source string, quiet bool) formatter.Format {
|
||||
return formatter.Format(source)
|
||||
}
|
||||
|
||||
// formatWrite writes the context
|
||||
func formatWrite(fmtCtx formatter.Context, configs client.ConfigListResult) error {
|
||||
cCtx := &configContext{
|
||||
HeaderContext: formatter.HeaderContext{
|
||||
Header: formatter.SubHeaderContext{
|
||||
"ID": configIDHeader,
|
||||
"Name": formatter.NameHeader,
|
||||
"CreatedAt": configCreatedHeader,
|
||||
"UpdatedAt": configUpdatedHeader,
|
||||
"Labels": formatter.LabelsHeader,
|
||||
},
|
||||
},
|
||||
}
|
||||
return fmtCtx.Write(cCtx, func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, config := range configs.Items {
|
||||
// FormatWrite writes the context
|
||||
func FormatWrite(ctx formatter.Context, configs []swarm.Config) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, config := range configs {
|
||||
configCtx := &configContext{c: config}
|
||||
if err := format(configCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return ctx.Write(newConfigContext(), render)
|
||||
}
|
||||
|
||||
func newConfigContext() *configContext {
|
||||
cCtx := &configContext{}
|
||||
|
||||
cCtx.Header = formatter.SubHeaderContext{
|
||||
"ID": configIDHeader,
|
||||
"Name": formatter.NameHeader,
|
||||
"CreatedAt": configCreatedHeader,
|
||||
"UpdatedAt": configUpdatedHeader,
|
||||
"Labels": formatter.LabelsHeader,
|
||||
}
|
||||
return cCtx
|
||||
}
|
||||
|
||||
type configContext struct {
|
||||
@ -98,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, ",")
|
||||
}
|
||||
@ -112,12 +115,12 @@ func (c *configContext) Label(name string) string {
|
||||
return c.c.Spec.Annotations.Labels[name]
|
||||
}
|
||||
|
||||
// inspectFormatWrite renders the context for a list of configs
|
||||
func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error {
|
||||
if fmtCtx.Format != configInspectPrettyTemplate {
|
||||
return inspect.Inspect(fmtCtx.Output, refs, string(fmtCtx.Format), getRef)
|
||||
// InspectFormatWrite renders the context for a list of configs
|
||||
func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.GetRefFunc) error {
|
||||
if ctx.Format != configInspectPrettyTemplate {
|
||||
return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef)
|
||||
}
|
||||
return fmtCtx.Write(&configInspectContext{}, func(format func(subContext formatter.SubContext) error) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, ref := range refs {
|
||||
configI, _, err := getRef(ref)
|
||||
if err != nil {
|
||||
@ -132,7 +135,8 @@ func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return ctx.Write(&configInspectContext{}, render)
|
||||
}
|
||||
|
||||
type configInspectContext struct {
|
||||
@ -153,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 {
|
||||
|
||||
@ -6,8 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@ -27,47 +26,36 @@ func TestConfigContextFormatWrite(t *testing.T) {
|
||||
`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
|
||||
`,
|
||||
},
|
||||
`},
|
||||
}
|
||||
|
||||
res := client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
{
|
||||
ID: "1",
|
||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}},
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}},
|
||||
},
|
||||
},
|
||||
configs := []swarm.Config{
|
||||
{ID: "1",
|
||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}}},
|
||||
{ID: "2",
|
||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
tc.context.Output = &out
|
||||
if err := formatWrite(tc.context, res); err != nil {
|
||||
if err := FormatWrite(tc.context, configs); err != nil {
|
||||
assert.ErrorContains(t, err, tc.expected)
|
||||
} else {
|
||||
assert.Equal(t, out.String(), tc.expected)
|
||||
|
||||
@ -1,73 +1,68 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.24
|
||||
|
||||
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/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// inspectOptions contains options for the docker config inspect command.
|
||||
type inspectOptions struct {
|
||||
names []string
|
||||
format string
|
||||
pretty bool
|
||||
// InspectOptions contains options for the docker config inspect command.
|
||||
type InspectOptions struct {
|
||||
Names []string
|
||||
Format string
|
||||
Pretty bool
|
||||
}
|
||||
|
||||
func newConfigInspectCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
opts := inspectOptions{}
|
||||
func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts := InspectOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [OPTIONS] CONFIG [CONFIG...]",
|
||||
Short: "Display detailed information on one or more configs",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.names = args
|
||||
return runInspect(cmd.Context(), dockerCLI, opts)
|
||||
opts.Names = args
|
||||
return RunConfigInspect(dockerCli, opts)
|
||||
},
|
||||
ValidArgsFunction: completeNames(dockerCLI),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp)
|
||||
cmd.Flags().BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format")
|
||||
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
|
||||
}
|
||||
|
||||
// runInspect inspects the given Swarm config.
|
||||
func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
// RunConfigInspect inspects the given Swarm config.
|
||||
func RunConfigInspect(dockerCli command.Cli, opts InspectOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
if opts.pretty {
|
||||
opts.format = "pretty"
|
||||
if opts.Pretty {
|
||||
opts.Format = "pretty"
|
||||
}
|
||||
|
||||
getRef := func(id string) (any, []byte, error) {
|
||||
res, err := apiClient.ConfigInspect(ctx, id, client.ConfigInspectOptions{})
|
||||
return res.Config, res.Raw, err
|
||||
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(opts.format, "pretty") && opts.format != "pretty" {
|
||||
return errors.New("cannot supply extra formatting options to the pretty template")
|
||||
if strings.HasPrefix(f, "pretty") && f != "pretty" {
|
||||
return fmt.Errorf("Cannot supply extra formatting options to the pretty template")
|
||||
}
|
||||
|
||||
configCtx := formatter.Context{
|
||||
Output: dockerCLI.Out(),
|
||||
Format: newFormat(opts.format, false),
|
||||
Output: dockerCli.Out(),
|
||||
Format: NewFormat(f, false),
|
||||
}
|
||||
|
||||
if err := inspectFormatWrite(configCtx, opts.names, getRef); err != nil {
|
||||
if err := InspectFormatWrite(configCtx, opts.Names, getRef); err != nil {
|
||||
return cli.StatusError{StatusCode: 1, Status: err.Error()}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
@ -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/moby/moby/client"
|
||||
. "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, _ client.ConfigInspectOptions) (client.ConfigInspectResult, 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, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
return client.ConfigInspectResult{}, 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",
|
||||
},
|
||||
@ -41,13 +40,11 @@ func TestConfigInspectErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
configInspectFunc: func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
configInspectFunc: func(configID string) (swarm.Config, []byte, error) {
|
||||
if configID == "foo" {
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(builders.ConfigName("foo")),
|
||||
}, nil
|
||||
return *Config(ConfigName("foo")), nil, nil
|
||||
}
|
||||
return client.ConfigInspectResult{}, errors.New("error while inspecting the config")
|
||||
return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
|
||||
},
|
||||
expectedError: "error while inspecting the config",
|
||||
},
|
||||
@ -60,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)
|
||||
}
|
||||
}
|
||||
@ -72,34 +68,25 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
configInspectFunc func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
configInspectFunc func(configID string) (swarm.Config, []byte, error)
|
||||
}{
|
||||
{
|
||||
name: "single-config",
|
||||
args: []string{"foo"},
|
||||
configInspectFunc: func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
configInspectFunc: func(name string) (swarm.Config, []byte, error) {
|
||||
if name != "foo" {
|
||||
return client.ConfigInspectResult{}, fmt.Errorf("invalid name, expected %s, got %s", "foo", name)
|
||||
return swarm.Config{}, nil, errors.Errorf("Invalid name, expected %s, got %s", "foo", name)
|
||||
}
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(
|
||||
builders.ConfigID("ID-foo"),
|
||||
builders.ConfigName("foo"),
|
||||
),
|
||||
}, 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, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(
|
||||
builders.ConfigID("ID-"+name),
|
||||
builders.ConfigName(name),
|
||||
builders.ConfigLabels(map[string]string{"label1": "label-foo"}),
|
||||
),
|
||||
}, nil
|
||||
configInspectFunc: func(name string) (swarm.Config, []byte, error) {
|
||||
return *Config(ConfigID("ID-"+name), ConfigName(name), ConfigLabels(map[string]string{
|
||||
"label1": "label-foo",
|
||||
})), nil, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -113,19 +100,16 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigInspectWithFormat(t *testing.T) {
|
||||
configInspectFunc := func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(
|
||||
builders.ConfigName("foo"),
|
||||
builders.ConfigLabels(map[string]string{"label1": "label-foo"}),
|
||||
),
|
||||
}, nil
|
||||
configInspectFunc := func(name string) (swarm.Config, []byte, error) {
|
||||
return *Config(ConfigName("foo"), ConfigLabels(map[string]string{
|
||||
"label1": "label-foo",
|
||||
})), nil, nil
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
format string
|
||||
args []string
|
||||
configInspectFunc func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
configInspectFunc func(name string) (swarm.Config, []byte, error)
|
||||
}{
|
||||
{
|
||||
name: "simple-template",
|
||||
@ -146,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))
|
||||
}
|
||||
@ -155,23 +139,21 @@ func TestConfigInspectWithFormat(t *testing.T) {
|
||||
func TestConfigInspectPretty(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
configInspectFunc func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
configInspectFunc func(string) (swarm.Config, []byte, error)
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
configInspectFunc: func(_ context.Context, id string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(
|
||||
builders.ConfigLabels(map[string]string{
|
||||
"lbl1": "value1",
|
||||
}),
|
||||
builders.ConfigID("configID"),
|
||||
builders.ConfigName("configName"),
|
||||
builders.ConfigCreatedAt(time.Time{}),
|
||||
builders.ConfigUpdatedAt(time.Time{}),
|
||||
builders.ConfigData([]byte("payload here")),
|
||||
),
|
||||
}, nil
|
||||
configInspectFunc: func(id string) (swarm.Config, []byte, error) {
|
||||
return *Config(
|
||||
ConfigLabels(map[string]string{
|
||||
"lbl1": "value1",
|
||||
}),
|
||||
ConfigID("configID"),
|
||||
ConfigName("configName"),
|
||||
ConfigCreatedAt(time.Time{}),
|
||||
ConfigUpdatedAt(time.Time{}),
|
||||
ConfigData([]byte("payload here")),
|
||||
), []byte{}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -182,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))
|
||||
}
|
||||
|
||||
@ -7,22 +7,21 @@ import (
|
||||
"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/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// listOptions contains options for the docker config ls command.
|
||||
type listOptions struct {
|
||||
quiet bool
|
||||
format string
|
||||
filter opts.FilterOpt
|
||||
// ListOptions contains options for the docker config ls command.
|
||||
type ListOptions struct {
|
||||
Quiet bool
|
||||
Format string
|
||||
Filter opts.FilterOpt
|
||||
}
|
||||
|
||||
func newConfigListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
listOpts := listOptions{filter: opts.NewFilterOpt()}
|
||||
func newConfigListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
listOpts := ListOptions{Filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls [OPTIONS]",
|
||||
@ -30,45 +29,44 @@ func newConfigListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Short: "List configs",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runList(cmd.Context(), dockerCLI, listOpts)
|
||||
return RunConfigList(dockerCli, listOpts)
|
||||
},
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&listOpts.quiet, "quiet", "q", false, "Only display IDs")
|
||||
flags.StringVar(&listOpts.format, "format", "", flagsHelper.FormatHelp)
|
||||
flags.VarP(&listOpts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
flags.BoolVarP(&listOpts.Quiet, "quiet", "q", false, "Only display IDs")
|
||||
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
|
||||
}
|
||||
|
||||
// runList lists Swarm configs.
|
||||
func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
// RunConfigList lists Swarm configs.
|
||||
func RunConfigList(dockerCli command.Cli, options ListOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
res, err := apiClient.ConfigList(ctx, client.ConfigListOptions{Filters: options.filter.Value()})
|
||||
configs, err := client.ConfigList(ctx, types.ConfigListOptions{Filters: options.Filter.Value()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
format := options.format
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(res.Items, func(i, j int) bool {
|
||||
return sortorder.NaturalLess(res.Items[i].Spec.Name, res.Items[j].Spec.Name)
|
||||
sort.Slice(configs, func(i, j int) bool {
|
||||
return sortorder.NaturalLess(configs[i].Spec.Name, configs[j].Spec.Name)
|
||||
})
|
||||
|
||||
configCtx := formatter.Context{
|
||||
Output: dockerCLI.Out(),
|
||||
Format: newFormat(format, options.quiet),
|
||||
Output: dockerCli.Out(),
|
||||
Format: NewFormat(format, options.Quiet),
|
||||
}
|
||||
return formatWrite(configCtx, res)
|
||||
return FormatWrite(configCtx, configs)
|
||||
}
|
||||
|
||||
@ -1,25 +1,25 @@
|
||||
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/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
. "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"
|
||||
)
|
||||
|
||||
func TestConfigListErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configListFunc func(context.Context, client.ConfigListOptions) (client.ConfigListResult, error)
|
||||
configListFunc func(types.ConfigListOptions) ([]swarm.Config, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -27,8 +27,8 @@ func TestConfigListErrors(t *testing.T) {
|
||||
expectedError: "accepts no argument",
|
||||
},
|
||||
{
|
||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
return client.ConfigListResult{}, errors.New("error listing configs")
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{}, errors.Errorf("error listing configs")
|
||||
},
|
||||
expectedError: "error listing configs",
|
||||
},
|
||||
@ -40,36 +40,33 @@ 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 client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
return client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-1-foo"),
|
||||
builders.ConfigName("1-foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 10}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*builders.Config(builders.ConfigID("ID-10-foo"),
|
||||
builders.ConfigName("10-foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*builders.Config(builders.ConfigID("ID-2-foo"),
|
||||
builders.ConfigName("2-foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
},
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*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)),
|
||||
),
|
||||
*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)),
|
||||
),
|
||||
*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
|
||||
},
|
||||
})
|
||||
@ -80,33 +77,29 @@ func TestConfigList(t *testing.T) {
|
||||
|
||||
func TestConfigListWithQuietOption(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
return client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
},
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*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 client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
return client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
},
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*Config(ConfigID("ID-foo"), ConfigName("foo")),
|
||||
*Config(ConfigID("ID-bar"), ConfigName("bar"), ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -120,49 +113,45 @@ func TestConfigListWithConfigFormat(t *testing.T) {
|
||||
|
||||
func TestConfigListWithFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
return client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
},
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*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 client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
assert.Check(t, options.Filters["name"]["foo"])
|
||||
assert.Check(t, options.Filters["label"]["lbl1=Label-bar"])
|
||||
return client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"),
|
||||
builders.ConfigName("foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 10}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*builders.Config(builders.ConfigID("ID-bar"),
|
||||
builders.ConfigName("bar"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
},
|
||||
configListFunc: func(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{
|
||||
*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)),
|
||||
),
|
||||
*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")
|
||||
}
|
||||
|
||||
@ -2,41 +2,54 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newConfigRemoveCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
// RemoveOptions contains options for the docker config rm command.
|
||||
type RemoveOptions struct {
|
||||
Names []string
|
||||
}
|
||||
|
||||
func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "rm CONFIG [CONFIG...]",
|
||||
Aliases: []string{"remove"},
|
||||
Short: "Remove one or more configs",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRemove(cmd.Context(), dockerCLI, args)
|
||||
opts := RemoveOptions{
|
||||
Names: args,
|
||||
}
|
||||
return RunConfigRemove(dockerCli, opts)
|
||||
},
|
||||
ValidArgsFunction: completeNames(dockerCLI),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
}
|
||||
|
||||
// runRemove removes the given Swarm configs.
|
||||
func runRemove(ctx context.Context, dockerCLI command.Cli, names []string) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
// RunConfigRemove removes the given Swarm configs.
|
||||
func RunConfigRemove(dockerCli command.Cli, opts RemoveOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []error
|
||||
for _, name := range names {
|
||||
if _, err := apiClient.ConfigRemove(ctx, name, client.ConfigRemoveOptions{}); err != nil {
|
||||
errs = append(errs, err)
|
||||
var errs []string
|
||||
|
||||
for _, name := range opts.Names {
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
@ -16,17 +14,17 @@ import (
|
||||
func TestConfigRemoveErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configRemoveFunc func(context.Context, string, client.ConfigRemoveOptions) (client.ConfigRemoveResult, error)
|
||||
configRemoveFunc func(string) error
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{},
|
||||
expectedError: "requires at least 1 argument",
|
||||
expectedError: "requires at least 1 argument.",
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
configRemoveFunc: func(ctx context.Context, name string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
||||
return client.ConfigRemoveResult{}, errors.New("error removing config")
|
||||
configRemoveFunc: func(name string) error {
|
||||
return errors.Errorf("error removing config")
|
||||
},
|
||||
expectedError: "error removing config",
|
||||
},
|
||||
@ -38,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)
|
||||
}
|
||||
}
|
||||
@ -48,9 +45,9 @@ func TestConfigRemoveWithName(t *testing.T) {
|
||||
names := []string{"foo", "bar"}
|
||||
var removedConfigs []string
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: func(_ context.Context, name string, _ client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
||||
configRemoveFunc: func(name string) error {
|
||||
removedConfigs = append(removedConfigs, name)
|
||||
return client.ConfigRemoveResult{}, nil
|
||||
return nil
|
||||
},
|
||||
})
|
||||
cmd := newConfigRemoveCommand(cli)
|
||||
@ -65,19 +62,18 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
|
||||
var removedConfigs []string
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: func(_ context.Context, name string, _ client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
||||
configRemoveFunc: func(name string) error {
|
||||
removedConfigs = append(removedConfigs, name)
|
||||
if name == "foo" {
|
||||
return client.ConfigRemoveResult{}, errors.New("error removing config: " + name)
|
||||
return errors.Errorf("error removing config: %s", name)
|
||||
}
|
||||
return client.ConfigRemoveResult{}, nil
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
@ -2,123 +2,111 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/moby/sys/signal"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"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, client.ContainerInspectOptions{})
|
||||
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
|
||||
}
|
||||
if !c.Container.State.Running {
|
||||
return nil, errors.New("cannot attach to a stopped container, start it first")
|
||||
if !c.State.Running {
|
||||
return nil, errors.New("You cannot attach to a stopped container, start it first")
|
||||
}
|
||||
if c.Container.State.Paused {
|
||||
return nil, errors.New("cannot attach to a paused container, unpause it first")
|
||||
if c.State.Paused {
|
||||
return nil, errors.New("You cannot attach to a paused container, unpause it first")
|
||||
}
|
||||
if c.Container.State.Restarting {
|
||||
return nil, errors.New("cannot attach to a restarting container, wait until it is running")
|
||||
if c.State.Restarting {
|
||||
return nil, errors.New("You cannot attach to a restarting container, wait until it is running")
|
||||
}
|
||||
|
||||
return &c.Container, nil
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// newAttachCommand creates a new cobra.Command for `docker attach`
|
||||
func newAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts AttachOptions
|
||||
// NewAttachCommand creates a new cobra.Command for `docker attach`
|
||||
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 != container.StatePaused
|
||||
}),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
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)
|
||||
waitRes := apiClient.ContainerWait(waitCtx, containerID, client.ContainerWaitOptions{})
|
||||
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 := client.ContainerAttachOptions{
|
||||
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 := notfiyAllSignals()
|
||||
go ForwardAllSignals(ctx, dockerCli, opts.container, sigc)
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
res, err := apiClient.ContainerAttach(ctx, containerID, options)
|
||||
if err != nil {
|
||||
return err
|
||||
resp, errAttach := client.ContainerAttach(ctx, opts.container, options)
|
||||
if errAttach != nil {
|
||||
return errAttach
|
||||
}
|
||||
defer res.HijackedResponse.Close()
|
||||
defer resp.Close()
|
||||
|
||||
// If use docker attach command to attach to a stop container, it will return
|
||||
// "You cannot attach to a stopped container" error, it's ok, but when
|
||||
@ -128,43 +116,42 @@ 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(),
|
||||
resp: res.HijackedResponse,
|
||||
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(waitRes)
|
||||
return getExitStatus(errC, resultC)
|
||||
}
|
||||
|
||||
func getExitStatus(waitRes client.ContainerWaitResult) error {
|
||||
func getExitStatus(errC <-chan error, resultC <-chan container.ContainerWaitOKBody) error {
|
||||
select {
|
||||
case result := <-waitRes.Result:
|
||||
case result := <-resultC:
|
||||
if result.Error != nil {
|
||||
return errors.New(result.Error.Message)
|
||||
return fmt.Errorf(result.Error.Message)
|
||||
}
|
||||
if result.StatusCode != 0 {
|
||||
return cli.StatusError{StatusCode: int(result.StatusCode)}
|
||||
}
|
||||
case err := <-waitRes.Error:
|
||||
case err := <-errC:
|
||||
return err
|
||||
}
|
||||
|
||||
@ -177,7 +164,7 @@ func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
|
||||
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
|
||||
// resize it, then go back to normal. Without this, every attach after the first will
|
||||
// require the user to manually resize or hit enter.
|
||||
resizeTTYTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
|
||||
resizeTtyTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
|
||||
|
||||
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
|
||||
// to the actual size.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user